- Upgrades NIP-72 communities to NIP-22 comments

- Adds moderation queue feed
- Adds in-app approval procedures
- Adds drafts for community posts
- Redesigns About us section of communities.
This commit is contained in:
Vitor Pamplona
2025-08-26 20:10:04 -04:00
parent fd752a4997
commit aa80d1b84a
42 changed files with 1509 additions and 382 deletions
@@ -32,8 +32,12 @@ fun Event.isTaggedAddressableNotes(addressIds: Set<String>) = tags.isTaggedAddre
fun Event.isTaggedAddressableKind(kind: Int) = tags.isTaggedAddressableKind(kind)
fun Event.isTaggedAddressableKind(kindStr: String) = tags.isTaggedAddressableKind(kindStr)
fun Event.getTagOfAddressableKind(kind: Int) = tags.getTagOfAddressableKind(kind)
fun Event.getTagOfAddressableKind(kindStr: String) = tags.getTagOfAddressableKind(kindStr)
fun Event.taggedATags() = tags.taggedATags()
fun Event.firstTaggedATag() = tags.firstTaggedATag()
@@ -35,8 +35,12 @@ fun TagArray.isTaggedAddressableNotes(addressIds: Set<String>) = this.any(ATag::
fun TagArray.isTaggedAddressableKind(kind: Int) = this.any(ATag::isTaggedWithKind, kind.toString())
fun TagArray.isTaggedAddressableKind(kindStr: String) = this.any(ATag::isTaggedWithKind, kindStr)
fun TagArray.getTagOfAddressableKind(kind: Int) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kind.toString())
fun TagArray.getTagOfAddressableKind(kindStr: String) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kindStr.toString())
fun TagArray.taggedATags() = this.mapNotNull(ATag::parse)
fun TagArray.firstTaggedATag() = this.fastFirstNotNullOfOrNull(ATag::parse)
@@ -72,16 +72,18 @@ open class BaseThreadedEvent(
return newStyleReply ?: newStyleRoot ?: oldStylePositional
}
fun tagsWithoutCitations(): List<String> {
open fun tagsWithoutCitations(): List<String> {
val certainRepliesTo = markedReplyTos()
val uncertainRepliesTo = unmarkedReplyTos()
val tagAddresses =
taggedATags()
.filter {
it.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || it.kind != WikiNoteEvent.KIND)
.filter { aTag ->
aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND)
// removes forks from itself.
}.map { it.toTag() }
}.map {
it.toTag()
}
if (certainRepliesTo.isEmpty() && uncertainRepliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
@@ -25,6 +25,7 @@ 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.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.any
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
@@ -33,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
@@ -55,10 +57,12 @@ import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
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
import kotlin.collections.mapNotNull
@Immutable
class CommentEvent(
@@ -126,6 +130,28 @@ class CommentEvent(
return aHints + qHints + nip19Hints
}
fun rootEventIds() = tags.mapNotNull(RootEventTag::parseKey)
fun replyEventIds() = tags.mapNotNull(ReplyEventTag::parseKey)
fun rootAddressIds() = tags.mapNotNull(RootAddressTag::parseAddressId)
fun hasRootAddress(addressId: String) = tags.any(RootAddressTag::isTagged, addressId)
fun hasReplyAddress(addressId: String) = tags.any(ReplyAddressTag::isTagged, addressId)
fun replyAddressIds() = tags.mapNotNull(ReplyAddressTag::parseAddressId)
fun replyAddress() = tags.mapNotNull(ReplyAddressTag::parseAddress)
fun rootScopes() = tags.filter { RootIdentifierTag.match(it) || RootAddressTag.match(it) || RootEventTag.match(it) }
fun rootKinds() = tags.filter(RootKindTag::match)
fun directReplies() = tags.filter { ReplyIdentifierTag.match(it) || ReplyAddressTag.match(it) || ReplyEventTag.match(it) }
fun directKinds() = tags.filter(ReplyKindTag::match)
fun rootAuthor() = tags.firstNotNullOfOrNull(RootAuthorTag::parse)
fun replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse)
@@ -142,14 +168,6 @@ class CommentEvent(
fun replyAuthorHints() = tags.mapNotNull(ReplyAuthorTag::parseAsHint)
fun rootScopes() = tags.filter { RootIdentifierTag.match(it) || RootAddressTag.match(it) || RootEventTag.match(it) }
fun rootKinds() = tags.filter(RootKindTag::match)
fun directReplies() = tags.filter { ReplyIdentifierTag.match(it) || ReplyAddressTag.match(it) || ReplyEventTag.match(it) }
fun directKinds() = tags.filter(ReplyKindTag::match)
/** root and reply scope search */
fun isTaggedScope(scopeId: String) = tags.any { RootIdentifierTag.isTagged(it, scopeId) || ReplyIdentifierTag.isTagged(it, scopeId) }
@@ -164,9 +182,9 @@ class CommentEvent(
fun isScoped(scopeTest: (String) -> Boolean) = tags.any { RootIdentifierTag.isTagged(it, scopeTest) || ReplyIdentifierTag.isTagged(it, scopeTest) }
fun hasRootScopeKind(kind: String) = tags.any { RootKindTag.isKind(it, kind) }
fun hasRootScopeKind(kind: String) = tags.any(RootKindTag::isKind, kind)
fun hasReplyScopeKind(kind: String) = tags.any { ReplyKindTag.isKind(it, kind) }
fun hasReplyScopeKind(kind: String) = tags.any(ReplyKindTag::isKind, kind)
fun hasScopeKind(kind: String) = tags.any { RootKindTag.isKind(it, kind) || ReplyKindTag.isKind(it, kind) }
@@ -184,12 +202,31 @@ class CommentEvent(
tags.lastNotNullOfOrNull(ReplyEventTag::parseKey)
?: tags.lastNotNullOfOrNull(RootEventTag::parseKey)
fun rootAddress() = tags.mapNotNull(RootAddressTag::parseAddress)
fun rootAddressId() = tags.mapNotNull(RootAddressTag::parseAddressId)
fun replyingToAddressId(): String? =
tags.lastNotNullOfOrNull(RootAddressTag::parseAddressId)
?: tags.lastNotNullOfOrNull(ReplyAddressTag::parseAddressId)
tags.lastNotNullOfOrNull(ReplyAddressTag::parseAddressId)
?: tags.lastNotNullOfOrNull(RootAddressTag::parseAddressId)
override fun replyingToAddressOrEvent(): HexKey? = replyingToAddressId() ?: replyingTo()
override fun tagsWithoutCitations(): List<String> {
val rootAddress = rootAddressIds()
val replyAddress = replyAddressIds()
if (
rootAddress.any { Address.isOfKind(it, CommunityDefinitionEvent.KIND_STR) } &&
replyAddress.any { Address.isOfKind(it, CommunityDefinitionEvent.KIND_STR) }
) {
// this is the root of the community post
return emptyList()
}
return rootAddress + replyAddress + rootEventIds() + replyEventIds()
}
companion object {
const val KIND = 1111
const val ALT = "Reply to "
@@ -44,6 +44,12 @@ class ReplyAddressTag(
@JvmStatic
fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
@JvmStatic
fun isTagged(
tag: Array<String>,
addressId: String,
) = tag.has(1) && tag[0] == RootAddressTag.Companion.TAG_NAME && tag[1] == addressId
@JvmStatic
fun parse(tag: Array<String>): ReplyAddressTag? {
ensure(tag.has(1)) { return null }
@@ -55,6 +61,15 @@ class ReplyAddressTag(
return ReplyAddressTag(tag[1], relayHint)
}
@JvmStatic
fun parseAddress(tag: Array<String>): Address? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return Address.parse(tag[1])
}
@JvmStatic
fun parseAddressId(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
@@ -67,6 +67,15 @@ class RootAddressTag(
return RootAddressTag(tag[1], relayHint)
}
@JvmStatic
fun parseAddress(tag: Array<String>): Address? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return Address.parse(tag[1])
}
@JvmStatic
fun parseValidAddress(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
@@ -0,0 +1,58 @@
/**
* 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.nip72ModCommunities
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.any
import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
fun Event.isACommunityPost(): Boolean =
if (this is CommentEvent) {
this.isCommunityScoped()
} else {
this.tags.any(CommunityTag::isTaggedWithKind, CommunityDefinitionEvent.KIND_STR)
}
fun Event.isForCommunity(communityAddressId: String): Boolean =
if (this is CommentEvent) {
this.hasRootAddress(communityAddressId)
} else {
tags.any(CommunityTag::isTagged, communityAddressId)
}
fun Event.communityAddress(): Address? =
if (this is CommentEvent) {
this.rootAddress().firstNotNullOfOrNull {
if (it.kind == CommunityDefinitionEvent.KIND) it else null
}
} else {
this.tags.fastFirstNotNullOfOrNull(CommunityTag::parseAddress)
}
fun CommentEvent.isCommunityScoped() = hasRootScopeKind(CommunityDefinitionEvent.KIND_STR)
fun CommentEvent.communityScope() = this.tags.fastFirstNotNullOfOrNull(CommunityTag::parseAddress)
fun CommentEvent.communityScopes() = this.tags.mapNotNull(CommunityTag::parseAddress)
@@ -25,22 +25,29 @@ import androidx.compose.runtime.Immutable
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.any
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
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.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.isTagged
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag.Companion.parseAddress
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag.Companion.parseAddressId
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag.Companion.parseAsHint
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag.Companion.parse
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag.Companion.parseAsHint
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedEventTag.Companion.parseId
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.mapNotNull
@Immutable
class CommunityPostApprovalEvent(
@@ -54,13 +61,17 @@ class CommunityPostApprovalEvent(
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint)
override fun eventHints() = tags.mapNotNull(ApprovedEventTag::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + tags.mapNotNull(QTag::parseEventId)
override fun linkedEventIds() = tags.mapNotNull(ApprovedEventTag::parseId)
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint)
override fun addressHints() =
tags.mapNotNull(ApprovedAddressTag::parseAsHint) +
tags.mapNotNull(CommunityTag::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + tags.mapNotNull(QTag::parseAddressId)
override fun linkedAddressIds() =
tags.mapNotNull(ApprovedAddressTag::parseAddressId) +
tags.mapNotNull(CommunityTag::parseAddressId)
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
@@ -77,15 +88,17 @@ class CommunityPostApprovalEvent(
null
}
fun communities() = taggedATags().filter { it.kind == CommunityDefinitionEvent.KIND }
fun isForCommunity(communityAddressId: String): Boolean = tags.any(CommunityTag::isTagged, communityAddressId)
fun communityAddresses() = taggedAddresses().filter { it.kind == CommunityDefinitionEvent.KIND }
fun isForCommunity(community: Address): Boolean = isForCommunity(community.toValue())
fun approvedEvents() = taggedEvents()
fun communities(): List<CommunityTag> = tags.mapNotNull(CommunityTag::parse)
fun approvedATags() = taggedATags().filter { it.kind != CommunityDefinitionEvent.KIND }
fun communityAddresses(): List<Address> = tags.mapNotNull(CommunityTag::parseAddress)
fun approvedAddresses() = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.KIND }
fun approvedEvents(): List<HexKey> = tags.mapNotNull(ApprovedEventTag::parseId)
fun approvedAddresses(): List<Address> = tags.mapNotNull(ApprovedAddressTag::parseAddress)
companion object {
const val KIND = 4550
@@ -0,0 +1,142 @@
/**
* 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.nip72ModCommunities.approval.tags
import com.vitorpamplona.quartz.nip01Core.core.HexKey
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.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.ensure
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
class ApprovedAddressTag(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) {
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
fun toTagArray() = assemble(address, relayHint)
fun toTagIdOnly() = assemble(address, null)
companion object {
const val TAG_NAME = "a"
@JvmStatic
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && !Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
@JvmStatic
fun isTagged(
tag: Array<String>,
addressId: String,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId
@JvmStatic
fun isTagged(
tag: Array<String>,
address: ApprovedAddressTag,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
@JvmStatic
fun isIn(
tag: Array<String>,
addressIds: Set<String>,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
@JvmStatic
fun parse(tag: Array<String>): ApprovedAddressTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
val address = Address.parse(tag[1]) ?: return null
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
return ApprovedAddressTag(address, relayHint)
}
@JvmStatic
fun parseValidAddress(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
return Address.parse(tag[1])?.toValue()
}
@JvmStatic
fun parseAddress(tag: Array<String>): Address? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val address = Address.parse(tag[1]) ?: return null
ensure(address.kind != CommunityDefinitionEvent.KIND) { return null }
return address
}
@JvmStatic
fun parseAddressId(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
return tag[1]
}
@JvmStatic
fun parseAsHint(tag: Array<String>): AddressHint? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
ensure(tag[1].contains(':')) { return null }
ensure(tag[2].isNotEmpty()) { return null }
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
ensure(relayHint != null) { return null }
return AddressHint(tag[1], relayHint)
}
@JvmStatic
fun assemble(
aTagId: HexKey,
relay: NormalizedRelayUrl?,
) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url)
@JvmStatic
fun assemble(
address: Address,
relay: NormalizedRelayUrl?,
) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url)
@JvmStatic
fun assemble(
kind: Int,
pubKey: String,
dTag: String,
relay: NormalizedRelayUrl?,
) = assemble(Address.assemble(kind, pubKey, dTag), relay)
}
}
@@ -0,0 +1,109 @@
/**
* 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.nip72ModCommunities.approval.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Tag
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.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@Immutable
class ApprovedEventTag(
val ref: EventReference,
) {
constructor(eventId: String, relayHint: NormalizedRelayUrl?, pubkey: String?) : this(EventReference(eventId, pubkey, relayHint))
fun toTagArray() = assemble(ref)
companion object {
const val TAG_NAME = "e"
@JvmStatic
fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
@JvmStatic
fun isTagged(
tag: Array<String>,
eventId: String,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId
@JvmStatic
fun isIn(
tag: Array<String>,
eventIds: Set<String>,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in eventIds
@JvmStatic
fun parse(tag: Array<String>): ApprovedEventTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
return ApprovedEventTag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, tag.getOrNull(3))
}
@JvmStatic
fun parseId(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
return tag[1]
}
@JvmStatic
fun parseValidId(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
ensure(Hex.isHex(tag[1])) { return null }
return tag[1]
}
@JvmStatic
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 }
ensure(tag[2].isNotEmpty()) { return null }
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
ensure(relayHint != null) { return null }
return EventIdHint(tag[1], relayHint)
}
@JvmStatic
fun assemble(
eventId: HexKey,
relay: NormalizedRelayUrl?,
pubkey: String?,
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, pubkey)
@JvmStatic
fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author)
}
}
@@ -80,7 +80,7 @@ class CommunityDefinitionEvent(
fun moderators() = tags.mapNotNull(ModeratorTag::parse)
fun moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey)
fun moderatorKeys() = tags.mapNotNull(ModeratorTag::parseKey).plus(pubKey)
fun relays() = tags.mapNotNull(RelayTag::parse)
@@ -88,6 +88,7 @@ class CommunityDefinitionEvent(
companion object {
const val KIND = 34550
const val KIND_STR = "34550"
const val ALT_DESCRIPTION = "Community definition"
fun build(
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.ensure
@@ -47,7 +48,7 @@ class CommunityTag(
const val TAG_NAME = "a"
@JvmStatic
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
@JvmStatic
fun isTagged(
@@ -61,12 +62,6 @@ class CommunityTag(
address: CommunityTag,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
@JvmStatic
fun isIn(
tag: Array<String>,
addressIds: Set<String>,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
@JvmStatic
fun isTaggedWithKind(
tag: Array<String>,
@@ -74,26 +69,27 @@ class CommunityTag(
) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind)
@JvmStatic
fun parse(
aTagId: String,
relay: String?,
) = Address.parse(aTagId)?.let {
CommunityTag(it, relay?.let { RelayUrlNormalizer.normalizeOrNull(it) })
}
fun isIn(
tag: Array<String>,
addressIds: Set<String>,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
@JvmStatic
fun parse(tag: Array<String>): CommunityTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return parse(tag[1], tag.getOrNull(2))
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
val address = Address.parse(tag[1]) ?: return null
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
return CommunityTag(address, relayHint)
}
@JvmStatic
fun parseValidAddress(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
return Address.parse(tag[1])?.toValue()
}
@@ -102,14 +98,16 @@ class CommunityTag(
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return Address.parse(tag[1])
val address = Address.parse(tag[1]) ?: return null
ensure(address.kind == CommunityDefinitionEvent.KIND) { return null }
return address
}
@JvmStatic
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 }
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
return tag[1]
}
@@ -117,8 +115,7 @@ class CommunityTag(
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 }
ensure(tag[1].contains(':')) { return null }
ensure(Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
ensure(tag[2].isNotEmpty()) { return null }
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])