feat: make polls, code snippets, audio, git/torrent replies & interest sets searchable

Adds SearchableEvent to more text-bearing kinds:

- PollEvent (1068, NIP-88): question (content) + option labels
- CodeSnippetEvent (1337, NIP-C0): name, description + code
- GitReplyEvent (1622) and TorrentCommentEvent (2004): reply/comment content
- AudioHeaderEvent (1808): description (content)
- AudioTrackEvent (31337): subject
- InterestSetEvent (30015): title, description + public interest hashtags
  (private hashtags in NIP-44 content are not indexed)
- SoftwareReleaseEvent (30063): release notes (content) — note: kind 30063 is
  registered to NIP-51 ReleaseArtifactSetEvent in EventFactory, so this is for
  completeness and not exercised at runtime.

CommunityRulesEvent (34551) was intentionally left out: it is the NIP-9B
machine-readable rules companion; the human-readable community rules live on
CommunityDefinitionEvent.rules(), which is already indexed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
This commit is contained in:
Claude
2026-06-18 20:21:08 +00:00
parent 79f6bf1b7c
commit 340916aa86
8 changed files with 59 additions and 8 deletions
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -39,7 +40,10 @@ class AudioHeaderEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = content
fun download() = tags.firstNotNullOfOrNull(DownloadUrlTag::parse)
fun stream() = tags.firstNotNullOfOrNull(StreamUrlTag::parse)
@@ -31,7 +31,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip14Subject.SubjectTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -44,7 +46,13 @@ class AudioTrackEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
// content is empty for audio tracks; the only free-text is the subject tag.
override fun indexableContent() = listOfNotNull(subject()).joinToString("\n")
fun subject() = tags.firstNotNullOfOrNull(SubjectTag::parse)
fun participants() = tags.mapNotNull(ParticipantTag::parse)
fun type() = tags.firstNotNullOfOrNull(TypeTag::parse)
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
/**
@@ -55,7 +56,13 @@ class SoftwareReleaseEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider {
EventHintProvider,
SearchableEvent {
// content carries the release notes (free-text). NOTE: kind 30063 collides
// with NIP-51 ReleaseArtifactSetEvent, which is what EventFactory builds for
// stored events — so in practice this override is not exercised at runtime.
override fun indexableContent() = content
override fun eventHints() = tags.mapNotNull(AssetTag::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(AssetTag::parseId)
@@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints
import com.vitorpamplona.quartz.nip19Bech32.pubKeys
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
@@ -62,7 +63,10 @@ class GitReplyEvent(
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider,
EventHintProvider,
AddressHintProvider {
AddressHintProvider,
SearchableEvent {
override fun indexableContent() = content
override fun eventHints(): List<EventIdHint> {
val eHints = tags.mapNotNull(ETag::parseAsHint)
val qHints = tags.mapNotNull(QTag::parseEventAsHint)
@@ -48,6 +48,7 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds
import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints
import com.vitorpamplona.quartz.nip19Bech32.pubKeys
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -62,7 +63,10 @@ class TorrentCommentEvent(
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
PubKeyHintProvider,
AddressHintProvider {
AddressHintProvider,
SearchableEvent {
override fun indexableContent() = content
override fun eventHints(): List<EventIdHint> {
val eHints = tags.mapNotNull(MarkedETag::parseAsHint)
val qHints = tags.mapNotNull(QTag::parseEventAsHint)
@@ -33,10 +33,13 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.removeAny
import com.vitorpamplona.quartz.nip51Lists.removeIgnoreCase
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -49,7 +52,16 @@ class InterestSetEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
// Public title/description plus the public-interest hashtags. Private
// hashtags live in NIP-44 encrypted content and are never indexed.
override fun indexableContent() = (listOfNotNull(title(), description()) + publicHashtags()).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun publicHashtags() = tags.mapNotNull(HashtagTag::parse)
suspend fun privateHashtags(signer: NostrSigner) = privateTags(signer)?.mapNotNull(HashtagTag::parse)
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -41,7 +42,14 @@ class PollEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope {
RootScope,
SearchableEvent {
override fun indexableContent() =
buildString {
append(content)
options().forEach { append('\n').append(it.label) }
}
fun options() = tags.options()
fun relays() = tags.relays()
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
/**
@@ -43,7 +44,10 @@ class CodeSnippetEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope {
RootScope,
SearchableEvent {
override fun indexableContent() = listOfNotNull(snippetName(), snippetDescription(), content).joinToString("\n")
/** Programming language, lowercase (e.g. "python"). */
fun language() = tags.language()