Normalizes NIP-22 viewmodel to its own entity

This commit is contained in:
Vitor Pamplona
2025-05-20 14:23:51 -04:00
parent bb1dfee35b
commit 195e268865
24 changed files with 519 additions and 922 deletions
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
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.geohash.GeoHashTag
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAddressTag
@@ -44,6 +45,7 @@ import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
@@ -172,10 +174,18 @@ class CommentEvent(
) = eventTemplate(KIND, msg, createdAt) {
alt(ALT + extId.toScope())
rootExternalIdentity(extId)
if (extId is GeohashId) {
GeoHashTag.geoMipMap(extId.geohash).forEach { rootExternalIdentity(GeohashId(it, extId.hint)) }
} else {
rootExternalIdentity(extId)
}
rootKind(extId)
replyExternalIdentity(extId)
if (extId is GeohashId) {
GeoHashTag.geoMipMap(extId.geohash).forEach { replyExternalIdentity(GeohashId(it, extId.hint)) }
} else {
replyExternalIdentity(extId)
}
replyKind(extId)
initializer()
@@ -48,7 +48,9 @@ fun TagArrayBuilder<CommentEvent>.rootEvent(
pubkey: String?,
) = addUnique(RootEventTag.assemble(eventId, relayHint, pubkey))
fun TagArrayBuilder<CommentEvent>.rootExternalIdentity(id: ExternalId) = addAll(RootIdentifierTag.assemble(id))
fun TagArrayBuilder<CommentEvent>.rootExternalIdentity(id: ExternalId) = add(RootIdentifierTag.assemble(id))
fun TagArrayBuilder<CommentEvent>.rootExternalIdentities(ids: List<ExternalId>) = addAll(ids.map { RootIdentifierTag.assemble(it) })
fun TagArrayBuilder<CommentEvent>.rootKind(kind: String) = addUnique(RootKindTag.assemble(kind))
@@ -72,7 +74,9 @@ fun TagArrayBuilder<CommentEvent>.replyEvent(
pubkey: String?,
) = addUnique(ReplyEventTag.assemble(eventId, relayHint, pubkey))
fun TagArrayBuilder<CommentEvent>.replyExternalIdentity(id: ExternalId) = addAll(ReplyIdentifierTag.assemble(id))
fun TagArrayBuilder<CommentEvent>.replyExternalIdentity(id: ExternalId) = add(ReplyIdentifierTag.assemble(id))
fun TagArrayBuilder<CommentEvent>.replyExternalIdentities(ids: List<ExternalId>) = addAll(ids.map { ReplyIdentifierTag.assemble(it) })
fun TagArrayBuilder<CommentEvent>.replyKind(kind: String) = addUnique(ReplyKindTag.assemble(kind))
@@ -23,9 +23,17 @@ package com.vitorpamplona.quartz.nip22Comments.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Tag
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
import com.vitorpamplona.quartz.nip73ExternalIds.books.BookId
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.nip73ExternalIds.movies.MovieId
import com.vitorpamplona.quartz.nip73ExternalIds.papers.PaperId
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastEpisodeId
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastFeedId
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastPublisherId
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@@ -82,6 +90,26 @@ class ReplyIdentifierTag {
return tag[1]
}
@JvmStatic
fun parseExternalId(tag: Tag): ExternalId? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == RootIdentifierTag.Companion.TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val value = tag[1]
val hint = tag.getOrNull(2)
return BookId.parse(value, hint)
?: HashtagId.parse(value, hint)
?: GeohashId.parse(value, hint)
?: MovieId.parse(value, hint)
?: PaperId.parse(value, hint)
?: PodcastEpisodeId.parse(value, hint)
?: PodcastFeedId.parse(value, hint)
?: PodcastPublisherId.parse(value, hint)
?: UrlId.parse(value, hint)
}
@JvmStatic
fun assemble(
identity: String,
@@ -89,10 +117,6 @@ class ReplyIdentifierTag {
) = arrayOfNotNull(TAG_NAME, identity, hint)
@JvmStatic
fun assemble(id: ExternalId): List<Array<String>> =
when (id) {
is GeohashId -> GeoHashTag.geoMipMap(id.geohash).map { assemble(GeohashId.toScope(it), id.hint) }
else -> listOf(assemble(id.toScope(), id.hint()))
}
fun assemble(id: ExternalId): Array<String> = assemble(id.toScope(), id.hint())
}
}
@@ -23,9 +23,17 @@ package com.vitorpamplona.quartz.nip22Comments.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Tag
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
import com.vitorpamplona.quartz.nip73ExternalIds.books.BookId
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.nip73ExternalIds.movies.MovieId
import com.vitorpamplona.quartz.nip73ExternalIds.papers.PaperId
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastEpisodeId
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastFeedId
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastPublisherId
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@@ -81,6 +89,26 @@ class RootIdentifierTag<T : ExternalId> {
return tag[1]
}
@JvmStatic
fun parseExternalId(tag: Tag): ExternalId? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val value = tag[1]
val hint = tag.getOrNull(2)
return BookId.parse(value, hint)
?: HashtagId.parse(value, hint)
?: GeohashId.parse(value, hint)
?: MovieId.parse(value, hint)
?: PaperId.parse(value, hint)
?: PodcastEpisodeId.parse(value, hint)
?: PodcastFeedId.parse(value, hint)
?: PodcastPublisherId.parse(value, hint)
?: UrlId.parse(value, hint)
}
@JvmStatic
fun assemble(
identity: String,
@@ -88,10 +116,6 @@ class RootIdentifierTag<T : ExternalId> {
) = arrayOfNotNull(TAG_NAME, identity, hint)
@JvmStatic
fun assemble(id: ExternalId): List<Array<String>> =
when (id) {
is GeohashId -> GeoHashTag.geoMipMap(id.geohash).map { assemble(GeohashId.toScope(it), id.hint) }
else -> listOf(assemble(id.toScope(), id.hint()))
}
fun assemble(id: ExternalId): Array<String> = assemble(id.toScope(), id.hint())
}
}
@@ -0,0 +1,39 @@
/**
* 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.nip73ExternalIds
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
fun CommentEvent.scopes() = tags.mapNotNull { RootIdentifierTag.parseExternalId(it) }
fun CommentEvent.scope(): ExternalId? {
val scopes = scopes()
if (scopes.isEmpty()) return null
return if (scopes.first() is GeohashId) {
scopes.maxByOrNull { if (it is GeohashId) it.geohash else "" }
} else {
scopes().first()
}
}
@@ -54,5 +54,13 @@ class BookId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): BookId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return BookId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -53,5 +53,13 @@ class GeohashId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): GeohashId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return GeohashId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -80,5 +80,13 @@ class MovieId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): MovieId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return MovieId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -54,5 +54,13 @@ class PaperId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): PaperId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return PaperId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -53,5 +53,13 @@ class PodcastEpisodeId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): PodcastEpisodeId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return PodcastEpisodeId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -54,5 +54,13 @@ class PodcastFeedId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): PodcastFeedId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return PodcastFeedId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -54,5 +54,13 @@ class PodcastPublisherId(
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return encoded.substring(PREFIX_COLON.length)
}
fun parse(
encoded: String,
hint: String?,
): PodcastPublisherId? {
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
return PodcastPublisherId(encoded.substring(PREFIX_COLON.length), hint)
}
}
}
@@ -77,5 +77,16 @@ class HashtagId(
null
}
}
fun parse(
encoded: String,
hint: String?,
): HashtagId? {
return if (encoded.length > 1 && encoded[0] == KIND_CHR) {
HashtagId(encoded.substring(1), hint)
} else {
null
}
}
}
}
@@ -56,5 +56,13 @@ class UrlId(
ensure(encoded.startsWith(PREFIX1) || encoded.startsWith(PREFIX2)) { return null }
return encoded
}
fun parse(
encoded: String,
hint: String?,
): UrlId? {
ensure(encoded.startsWith(PREFIX1) || encoded.startsWith(PREFIX2)) { return null }
return UrlId(encoded, hint)
}
}
}