Merge pull request #1747 from vitorpamplona/claude/nip39-kind-10011-support-EACQk

Implement NIP-39 External Identities (kind 10011) event support
This commit is contained in:
Vitor Pamplona
2026-03-03 10:01:52 -05:00
committed by GitHub
12 changed files with 257 additions and 32 deletions
@@ -24,16 +24,17 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
fun MetadataEvent.identityClaims() = tags.mapNotNull(IdentityClaimTag::parse)
fun MetadataEvent.replaceClaims(
fun ExternalIdentitiesEvent.identityClaims() = tags.mapNotNull(IdentityClaimTag::parse)
fun List<IdentityClaimTag>.replaceClaims(
twitter: String?,
mastodon: String?,
github: String?,
): List<IdentityClaimTag> {
var claims = identityClaims()
var claims = this
// null leave as is. blank deletes it.
if (twitter != null) {
// delete twitter
claims = claims.filter { it !is TwitterIdentity }
if (twitter.isNotBlank()) {
TwitterIdentity.parseProofUrl(twitter)?.let { claims = claims + it }
@@ -42,7 +43,6 @@ fun MetadataEvent.replaceClaims(
// null leave as is. blank deletes it.
if (github != null) {
// delete github
claims = claims.filter { it !is GitHubIdentity }
if (github.isNotBlank()) {
GitHubIdentity.parseProofUrl(github)?.let { claims = claims + it }
@@ -59,3 +59,15 @@ fun MetadataEvent.replaceClaims(
return claims
}
fun MetadataEvent.replaceClaims(
twitter: String?,
mastodon: String?,
github: String?,
): List<IdentityClaimTag> = identityClaims().replaceClaims(twitter, mastodon, github)
fun ExternalIdentitiesEvent.replaceClaims(
twitter: String?,
mastodon: String?,
github: String?,
): List<IdentityClaimTag> = identityClaims().replaceClaims(twitter, mastodon, github)
@@ -0,0 +1,86 @@
/*
* 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.nip39ExtIdentities
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.builder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class ExternalIdentitiesEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 10011
const val ALT_DESCRIPTION = "External Identities"
const val FIXED_D_TAG = ""
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
fun createNew(
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<ExternalIdentitiesEvent>.() -> Unit = {},
): EventTemplate<ExternalIdentitiesEvent> =
eventTemplate(KIND, "", createdAt) {
alt(ALT_DESCRIPTION)
twitter?.let { twitterClaim(it) }
mastodon?.let { mastodonClaim(it) }
github?.let { githubClaim(it) }
initializer()
}
fun updateFromPast(
latest: ExternalIdentitiesEvent,
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<ExternalIdentitiesEvent>.() -> Unit = {},
): EventTemplate<ExternalIdentitiesEvent> {
val tags =
latest.tags.builder {
alt(ALT_DESCRIPTION)
val newClaims = latest.replaceClaims(twitter, mastodon, github)
remove(IdentityClaimTag.TAG_NAME)
claims(newClaims)
initializer()
}
return EventTemplate(createdAt, KIND, tags, "")
}
}
}
@@ -20,19 +20,19 @@
*/
package com.vitorpamplona.quartz.nip39ExtIdentities
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
fun TagArrayBuilder<MetadataEvent>.claims(identities: List<IdentityClaimTag>) = addAll(identities.map { it.toTagArray() })
fun <T : Event> TagArrayBuilder<T>.claims(identities: List<IdentityClaimTag>) = addAll(identities.map { it.toTagArray() })
fun TagArrayBuilder<MetadataEvent>.twitterClaim(twitter: TwitterIdentity) = add(twitter.toTagArray())
fun <T : Event> TagArrayBuilder<T>.twitterClaim(twitter: TwitterIdentity) = add(twitter.toTagArray())
fun TagArrayBuilder<MetadataEvent>.mastodonClaim(mastodon: MastodonIdentity) = add(mastodon.toTagArray())
fun <T : Event> TagArrayBuilder<T>.mastodonClaim(mastodon: MastodonIdentity) = add(mastodon.toTagArray())
fun TagArrayBuilder<MetadataEvent>.githubClaim(github: GitHubIdentity) = add(github.toTagArray())
fun <T : Event> TagArrayBuilder<T>.githubClaim(github: GitHubIdentity) = add(github.toTagArray())
fun TagArrayBuilder<MetadataEvent>.twitterClaim(twitterUrl: String) = TwitterIdentity.parseProofUrl(twitterUrl)?.let { twitterClaim(it) }
fun <T : Event> TagArrayBuilder<T>.twitterClaim(twitterUrl: String) = TwitterIdentity.parseProofUrl(twitterUrl)?.let { twitterClaim(it) }
fun TagArrayBuilder<MetadataEvent>.mastodonClaim(mastodonUrl: String) = MastodonIdentity.parseProofUrl(mastodonUrl)?.let { mastodonClaim(it) }
fun <T : Event> TagArrayBuilder<T>.mastodonClaim(mastodonUrl: String) = MastodonIdentity.parseProofUrl(mastodonUrl)?.let { mastodonClaim(it) }
fun TagArrayBuilder<MetadataEvent>.githubClaim(githubUrl: String) = GitHubIdentity.parseProofUrl(githubUrl)?.let { githubClaim(it) }
fun <T : Event> TagArrayBuilder<T>.githubClaim(githubUrl: String) = GitHubIdentity.parseProofUrl(githubUrl)?.let { githubClaim(it) }
@@ -73,6 +73,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
@@ -226,6 +227,7 @@ class EventFactory {
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig)
ExternalIdentitiesEvent.KIND -> ExternalIdentitiesEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)