mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
Merge pull request #3896 from vitorpamplona/claude/neo4j-quartz-graph-schema-z04j8o
Declare pointer hints on Report, ChatMessage, Classifieds and ChannelCreate events
This commit is contained in:
@@ -395,7 +395,7 @@ class CachePruner(
|
||||
|
||||
if (noteEvent is ReportEvent) {
|
||||
noteEvent.reportedAuthor().forEach {
|
||||
cache.getUserIfExists(it.pubkey)?.reportsOrNull()?.let { reports ->
|
||||
cache.getUserIfExists(it.pubKey)?.reportsOrNull()?.let { reports ->
|
||||
reports.removeReport(note)
|
||||
reports.removeReportNamingUser(note)
|
||||
}
|
||||
|
||||
@@ -1864,7 +1864,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
val new = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
if (new) {
|
||||
val authorsReported = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.pubkey) }
|
||||
val authorsReported = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.pubKey) }
|
||||
val eventsReported =
|
||||
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.eventId) } +
|
||||
event.reportedAddresses().map { getOrCreateAddressableNote(it.address) }
|
||||
@@ -1885,7 +1885,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
// report can `p`-tag an incidentally-mentioned third party with no type of its own,
|
||||
// and there is no threshold here to absorb that noise the way
|
||||
// `receivedReportsByAuthor`'s hide path does.
|
||||
val explicitlyTyped = event.reportedAuthorsWithOwnType().mapTo(mutableSetOf()) { it.pubkey }
|
||||
val explicitlyTyped = event.reportedAuthorsWithOwnType().mapTo(mutableSetOf()) { it.pubKey }
|
||||
authorsReported.forEach { author ->
|
||||
if (author.pubkeyHex in explicitlyTyped) author.reports().addReportNamingUser(note)
|
||||
}
|
||||
|
||||
+1
-1
@@ -303,7 +303,7 @@ object GrapeRankScore {
|
||||
dropped++
|
||||
continue
|
||||
}
|
||||
builder.addReports(r.pubKey, r.reportedAuthor().map { it.pubkey })
|
||||
builder.addReports(r.pubKey, r.reportedAuthor().map { it.pubKey })
|
||||
}
|
||||
if (dropped > 0) System.err.println("[graperank] dropped $dropped retracted reports (NIP-09 deletions)")
|
||||
return dropped
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ fun dmReportWarningFor(
|
||||
|
||||
(report.event as? ReportEvent)?.reportedAuthor()?.forEach {
|
||||
// A single report can name several people; only this counterpart's reason belongs here.
|
||||
if (it.pubkey == counterpart.pubkeyHex) it.type?.let(types::add)
|
||||
if (it.pubKey == counterpart.pubkeyHex) it.type?.let(types::add)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
@@ -41,10 +42,15 @@ class ChatMessageEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseDMGroupEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
SearchableEvent {
|
||||
// content is the decrypted (plaintext) direct-message body.
|
||||
override fun indexableContent() = content
|
||||
|
||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
fun replyTo() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
companion object {
|
||||
|
||||
+7
@@ -25,10 +25,12 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
@@ -46,9 +48,14 @@ class ChannelCreateEvent(
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = channelInfo().let { listOfNotNull(it.name, it.about, it.picture).joinToString(" ") }
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var cache: ChannelDataNorm? = null
|
||||
|
||||
@@ -26,6 +26,9 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.DefaultReportTag
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAddressTag
|
||||
@@ -42,7 +45,22 @@ class ReportEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PubKeyHintProvider,
|
||||
EventHintProvider,
|
||||
AddressHintProvider {
|
||||
override fun pubKeyHints() = tags.mapNotNull(ReportedAuthorTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(ReportedAuthorTag::parseKey)
|
||||
|
||||
override fun eventHints() = tags.mapNotNull(ReportedEventTag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ReportedEventTag::parseId)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ReportedAddressTag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ReportedAddressTag::parseAddressId)
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
private var defaultType: ReportType? = null
|
||||
|
||||
+7
-3
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip56Reports
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.HashSha256Tag
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAddressTag
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAuthorTag
|
||||
@@ -32,17 +33,20 @@ import com.vitorpamplona.quartz.nip56Reports.tags.ServerTag
|
||||
fun TagArrayBuilder<ReportEvent>.event(
|
||||
eventId: HexKey,
|
||||
reportType: ReportType,
|
||||
) = addUnique(ReportedEventTag.assemble(eventId, reportType))
|
||||
relay: NormalizedRelayUrl? = null,
|
||||
) = addUnique(ReportedEventTag.assemble(eventId, relay, reportType))
|
||||
|
||||
fun TagArrayBuilder<ReportEvent>.address(
|
||||
address: Address,
|
||||
reportType: ReportType,
|
||||
) = addUnique(ReportedAddressTag.assemble(address, reportType))
|
||||
relay: NormalizedRelayUrl? = null,
|
||||
) = addUnique(ReportedAddressTag.assemble(address, relay, reportType))
|
||||
|
||||
fun TagArrayBuilder<ReportEvent>.user(
|
||||
pubkey: HexKey,
|
||||
reportType: ReportType,
|
||||
) = addUnique(ReportedAuthorTag.assemble(pubkey, reportType))
|
||||
relay: NormalizedRelayUrl? = null,
|
||||
) = addUnique(ReportedAuthorTag.assemble(pubkey, relay, reportType))
|
||||
|
||||
fun TagArrayBuilder<ReportEvent>.hash(
|
||||
x: String,
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip56Reports.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
/**
|
||||
* Positional layout shared by the NIP-56 pointer tags (`p`, `e`, `a`).
|
||||
*
|
||||
* NIP-56 predates the convention that slot 2 of a pointer tag is a relay
|
||||
* hint: it put the report type there instead. Both layouts are in the wild,
|
||||
* so every reader has to disambiguate:
|
||||
*
|
||||
* ```
|
||||
* ["p", "<pubkey>", "nudity"] // legacy: type at 2
|
||||
* ["p", "<pubkey>", "wss://relay/", "nudity"] // modern: hint at 2, type at 3
|
||||
* ["p", "<pubkey>", "wss://relay/"] // modern, no per-tag type
|
||||
* ["p", "<pubkey>", "", "nudity"] // empty hint slot
|
||||
* ```
|
||||
*
|
||||
* Disambiguation is by *shape*, not by tag length: a slot that parses as a
|
||||
* relay URL is a hint, never a type. No report-type code is a relay URL, so
|
||||
* the two spaces cannot collide.
|
||||
*/
|
||||
internal object ReportTagLayout {
|
||||
/** The relay hint at slot 2, or null under the legacy layout. */
|
||||
fun relayHint(tag: Array<String>): NormalizedRelayUrl? {
|
||||
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) {
|
||||
return RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The report type this tag carries, falling back to [default] (the
|
||||
* event-level type) when the tag does not name one of its own.
|
||||
*
|
||||
* Only a slot that is neither blank nor a relay URL is offered to
|
||||
* [ReportType.parseOrNull] — which despite its name never returns null
|
||||
* and maps anything unrecognized to [ReportType.OTHER]. Feeding it a
|
||||
* relay URL would turn every hint-carrying tag into an `OTHER` report
|
||||
* and mask the event-level default.
|
||||
*/
|
||||
fun reportType(
|
||||
tag: Array<String>,
|
||||
default: ReportType?,
|
||||
): ReportType? {
|
||||
val slot = if (tag.has(2) && (tag[2].isBlank() || relayHint(tag) != null)) 3 else 2
|
||||
|
||||
if (!tag.has(slot) || tag[slot].isBlank()) return default
|
||||
|
||||
return ReportType.parseOrNull(tag[slot], tag) ?: default
|
||||
}
|
||||
}
|
||||
+41
-11
@@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip56Reports.tags
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
@@ -30,9 +32,10 @@ import com.vitorpamplona.quartz.utils.ensure
|
||||
@Immutable
|
||||
class ReportedAddressTag(
|
||||
val address: Address,
|
||||
val relay: NormalizedRelayUrl? = null,
|
||||
override val type: ReportType? = null,
|
||||
) : BaseReportTag {
|
||||
fun toTagArray() = assemble(address, type)
|
||||
fun toTagArray() = assemble(address, relay, type)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "a"
|
||||
@@ -49,21 +52,48 @@ class ReportedAddressTag(
|
||||
|
||||
ensure(address != null) { return null }
|
||||
|
||||
val type =
|
||||
if (tag.size == 2) {
|
||||
defaultReportType
|
||||
} else if (tag.size == 3) {
|
||||
ReportType.parseOrNull(tag[2], tag) ?: defaultReportType
|
||||
} else {
|
||||
ReportType.parseOrNull(tag[3], tag) ?: defaultReportType
|
||||
}
|
||||
return ReportedAddressTag(
|
||||
address,
|
||||
ReportTagLayout.relayHint(tag),
|
||||
ReportTagLayout.reportType(tag, defaultReportType),
|
||||
)
|
||||
}
|
||||
|
||||
return ReportedAddressTag(address, type)
|
||||
fun parseAddressId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return Address.parse(tag[1])?.toValue()
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
val address = Address.parse(tag[1])
|
||||
ensure(address != null) { return null }
|
||||
|
||||
val hint = ReportTagLayout.relayHint(tag)
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return AddressHint(address.toValue(), hint)
|
||||
}
|
||||
|
||||
/** See [ReportedAuthorTag.assemble] for why the layout is conditional. */
|
||||
fun assemble(
|
||||
address: Address,
|
||||
relay: NormalizedRelayUrl?,
|
||||
type: ReportType?,
|
||||
) = if (relay != null) {
|
||||
arrayOfNotNull(TAG_NAME, address.toValue(), relay.url, type?.code)
|
||||
} else {
|
||||
arrayOfNotNull(TAG_NAME, address.toValue(), type?.code)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
address: Address,
|
||||
type: ReportType? = null,
|
||||
) = arrayOfNotNull(TAG_NAME, address.toValue(), type?.code)
|
||||
) = assemble(address, null, type)
|
||||
}
|
||||
}
|
||||
|
||||
+48
-13
@@ -23,16 +23,21 @@ package com.vitorpamplona.quartz.nip56Reports.tags
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@Immutable
|
||||
class ReportedAuthorTag(
|
||||
val pubkey: HexKey,
|
||||
override val pubKey: HexKey,
|
||||
override val relayHint: NormalizedRelayUrl? = null,
|
||||
override val type: ReportType? = null,
|
||||
) : BaseReportTag {
|
||||
fun toTagArray() = assemble(pubkey, type)
|
||||
) : BaseReportTag,
|
||||
PubKeyReferenceTag {
|
||||
fun toTagArray() = assemble(pubKey, relayHint, type)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "p"
|
||||
@@ -45,21 +50,51 @@ class ReportedAuthorTag(
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val type =
|
||||
if (tag.size == 2) {
|
||||
defaultReportType
|
||||
} else if (tag.size == 3) {
|
||||
ReportType.parseOrNull(tag[2], tag) ?: defaultReportType
|
||||
} else {
|
||||
ReportType.parseOrNull(tag[3], tag) ?: defaultReportType
|
||||
}
|
||||
return ReportedAuthorTag(
|
||||
tag[1],
|
||||
ReportTagLayout.relayHint(tag),
|
||||
ReportTagLayout.reportType(tag, defaultReportType),
|
||||
)
|
||||
}
|
||||
|
||||
return ReportedAuthorTag(tag[1], type)
|
||||
fun parseKey(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Array<String>): PubKeyHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val hint = ReportTagLayout.relayHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return PubKeyHint(tag[1], hint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the legacy `[name, id, type]` layout unless a relay hint is
|
||||
* supplied. Padding slot 2 with `""` to always reach the modern
|
||||
* layout would be well-formed here but unreadable to the many
|
||||
* clients that still read the report type out of slot 2.
|
||||
*/
|
||||
fun assemble(
|
||||
pubkey: HexKey,
|
||||
relayHint: NormalizedRelayUrl?,
|
||||
type: ReportType?,
|
||||
) = if (relayHint != null) {
|
||||
arrayOfNotNull(TAG_NAME, pubkey, relayHint.url, type?.code)
|
||||
} else {
|
||||
arrayOfNotNull(TAG_NAME, pubkey, type?.code)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
pubkey: HexKey,
|
||||
type: ReportType? = null,
|
||||
) = arrayOfNotNull(TAG_NAME, pubkey, type?.code)
|
||||
) = assemble(pubkey, null, type)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-13
@@ -23,16 +23,24 @@ package com.vitorpamplona.quartz.nip56Reports.tags
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@Immutable
|
||||
class ReportedEventTag(
|
||||
val eventId: HexKey,
|
||||
override val eventId: HexKey,
|
||||
override val relay: NormalizedRelayUrl? = null,
|
||||
override val type: ReportType? = null,
|
||||
) : BaseReportTag {
|
||||
fun toTagArray() = assemble(eventId, type)
|
||||
) : BaseReportTag,
|
||||
GenericETag {
|
||||
/** NIP-56 `e` tags carry no author slot — slot 3 is the report type. */
|
||||
override val author: HexKey? = null
|
||||
|
||||
override fun toTagArray() = assemble(eventId, relay, type)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "e"
|
||||
@@ -45,21 +53,46 @@ class ReportedEventTag(
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val type =
|
||||
if (tag.size == 2) {
|
||||
defaultReportType
|
||||
} else if (tag.size == 3) {
|
||||
ReportType.parseOrNull(tag[2], tag) ?: defaultReportType
|
||||
} else {
|
||||
ReportType.parseOrNull(tag[3], tag) ?: defaultReportType
|
||||
}
|
||||
return ReportedEventTag(
|
||||
tag[1],
|
||||
ReportTagLayout.relayHint(tag),
|
||||
ReportTagLayout.reportType(tag, defaultReportType),
|
||||
)
|
||||
}
|
||||
|
||||
return ReportedEventTag(tag[1], type)
|
||||
fun parseId(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Array<String>): EventIdHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val hint = ReportTagLayout.relayHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return EventIdHint(tag[1], hint)
|
||||
}
|
||||
|
||||
/** See [ReportedAuthorTag.assemble] for why the layout is conditional. */
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
type: ReportType?,
|
||||
) = if (relay != null) {
|
||||
arrayOfNotNull(TAG_NAME, eventId, relay.url, type?.code)
|
||||
} else {
|
||||
arrayOfNotNull(TAG_NAME, eventId, type?.code)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
type: ReportType? = null,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, type?.code)
|
||||
) = assemble(eventId, null, type)
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -25,9 +25,15 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.containsAllTagNamesWithValues
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
|
||||
@@ -53,9 +59,24 @@ class ClassifiedsEvent(
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PublishedAtProvider,
|
||||
PubKeyHintProvider,
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = listOfNotNull(title(), summary(), content).joinToString("\n")
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip56Reports
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAddressTag
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAuthorTag
|
||||
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedEventTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ReportHintProviderTest {
|
||||
private val pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
|
||||
private val other = "99bb5591c9116600f845107d31f9b59e2f7c7e09a1ff802e84f1d43da557ca64"
|
||||
private val eventId = "43575072239da152afe3d7b5c70ed2beb48db2b10e60c60da45229c09c877d2a"
|
||||
private val addressId = "30402:$other:7e5fec50-9add-48c6-985c-eb593e6c14cd"
|
||||
private val relay = "wss://relay.damus.io/"
|
||||
|
||||
private fun report(vararg tags: Array<String>) =
|
||||
ReportEvent(
|
||||
id = "00".repeat(32),
|
||||
pubKey = pubkey,
|
||||
createdAt = 1700000000,
|
||||
tags = arrayOf(*tags),
|
||||
content = "",
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Layout disambiguation: legacy [name, id, type] vs modern
|
||||
// [name, id, relay, type]. See ReportTagLayout.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun legacyLayoutReadsTypeFromSlotTwo() {
|
||||
val tag = ReportedAuthorTag.parse(arrayOf("p", other, "nudity"))!!
|
||||
assertEquals(other, tag.pubKey)
|
||||
assertEquals(ReportType.NUDITY, tag.type)
|
||||
assertNull(tag.relayHint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun modernLayoutReadsHintFromTwoAndTypeFromThree() {
|
||||
val tag = ReportedAuthorTag.parse(arrayOf("p", other, relay, "impersonation"))!!
|
||||
assertEquals(other, tag.pubKey)
|
||||
assertEquals(ReportType.IMPERSONATION, tag.type)
|
||||
assertEquals(relay, tag.relayHint?.url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: a relay URL used to be fed to ReportType.parseOrNull,
|
||||
* which maps anything unrecognized to OTHER — so a hint-carrying tag
|
||||
* with no per-tag type silently became an `OTHER` report and masked
|
||||
* the event-level default.
|
||||
*/
|
||||
@Test
|
||||
fun relayHintWithoutTypeFallsBackToDefaultNotOther() {
|
||||
val tag = ReportedAuthorTag.parse(arrayOf("p", other, relay), ReportType.SPAM)!!
|
||||
assertEquals(relay, tag.relayHint?.url)
|
||||
assertEquals(ReportType.SPAM, tag.type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyHintSlotStillReadsTypeFromSlotThree() {
|
||||
val tag = ReportedAuthorTag.parse(arrayOf("p", other, "", "nudity"))!!
|
||||
assertEquals(ReportType.NUDITY, tag.type)
|
||||
assertNull(tag.relayHint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blankTypeSlotFallsBackToDefault() {
|
||||
val tag = ReportedAuthorTag.parse(arrayOf("p", other, ""), ReportType.MALWARE)!!
|
||||
assertEquals(ReportType.MALWARE, tag.type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun eventAndAddressTagsShareTheSameLayoutRules() {
|
||||
val e = ReportedEventTag.parse(arrayOf("e", eventId, relay, "illegal"))!!
|
||||
assertEquals(eventId, e.eventId)
|
||||
assertEquals(relay, e.relay?.url)
|
||||
assertEquals(ReportType.ILLEGAL, e.type)
|
||||
assertNull(e.author)
|
||||
|
||||
val a = ReportedAddressTag.parse(arrayOf("a", addressId, relay, "spam"))!!
|
||||
assertEquals(addressId, a.address.toValue())
|
||||
assertEquals(relay, a.relay?.url)
|
||||
assertEquals(ReportType.SPAM, a.type)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Round trip
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun assembleRoundTripsBothLayouts() {
|
||||
val legacy = ReportedAuthorTag.assemble(other, ReportType.SPAM)
|
||||
assertEquals(listOf("p", other, "spam"), legacy.toList())
|
||||
assertEquals(ReportType.SPAM, ReportedAuthorTag.parse(legacy)!!.type)
|
||||
|
||||
val modern = ReportedAuthorTag(other, RelayUrlNormalizer.normalizeOrNull(relay), ReportType.SPAM).toTagArray()
|
||||
assertEquals(listOf("p", other, relay, "spam"), modern.toList())
|
||||
|
||||
val reparsed = ReportedAuthorTag.parse(modern)!!
|
||||
assertEquals(ReportType.SPAM, reparsed.type)
|
||||
assertEquals(relay, reparsed.relayHint?.url)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Hint providers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun reportEventExposesEveryPointerItCarries() {
|
||||
val event =
|
||||
report(
|
||||
arrayOf("p", other, relay, "impersonation"),
|
||||
arrayOf("e", eventId, "nudity"),
|
||||
arrayOf("a", addressId, relay, "spam"),
|
||||
)
|
||||
|
||||
assertEquals(listOf(other), event.linkedPubKeys())
|
||||
assertEquals(listOf(eventId), event.linkedEventIds())
|
||||
assertEquals(listOf(addressId), event.linkedAddressIds())
|
||||
|
||||
// hints only where a relay is actually present
|
||||
assertEquals(listOf(other), event.pubKeyHints().map { it.pubkey })
|
||||
assertEquals(listOf(relay), event.pubKeyHints().map { it.relay.url })
|
||||
assertTrue(event.eventHints().isEmpty())
|
||||
assertEquals(listOf(addressId), event.addressHints().map { it.addressId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun linkedPubKeysMatchesTypedAccessorsSoTheGraphCannotDrift() {
|
||||
val event =
|
||||
report(
|
||||
arrayOf("p", other, "nudity"),
|
||||
arrayOf("p", pubkey, relay, "spam"),
|
||||
arrayOf("e", eventId, "nudity"),
|
||||
)
|
||||
|
||||
// The completeness invariant: every pubkey the typed accessor
|
||||
// reports is also reported by the generic hint provider.
|
||||
assertEquals(
|
||||
event.reportedAuthor().map { it.pubKey }.toSet(),
|
||||
event.linkedPubKeys().toSet(),
|
||||
)
|
||||
assertEquals(
|
||||
event.reportedPost().map { it.eventId }.toSet(),
|
||||
event.linkedEventIds().toSet(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun malformedPointersAreDropped() {
|
||||
val event =
|
||||
report(
|
||||
arrayOf("p", "tooshort", "nudity"),
|
||||
arrayOf("p"),
|
||||
arrayOf("e", eventId),
|
||||
arrayOf("a", "not-an-address"),
|
||||
)
|
||||
|
||||
assertTrue(event.linkedPubKeys().isEmpty())
|
||||
assertEquals(listOf(eventId), event.linkedEventIds())
|
||||
assertTrue(event.linkedAddressIds().isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user