feat: implement all missing NIP-51 list event kinds

Add 14 missing NIP-51 event kinds with full CRUD support:

Replaceable lists:
- 10009 SimpleGroupListEvent (NIP-29 group membership)
- 10017 GitAuthorListEvent (NIP-34 code follows)
- 10018 GitRepositoryListEvent (NIP-34 repo follows)
- 10020 MediaFollowListEvent (multimedia follows)
- 10101 GoodWikiAuthorListEvent (NIP-54 wiki authors)
- 10102 GoodWikiRelayListEvent (NIP-54 wiki relays)

Addressable sets:
- 30004 ArticleCurationSetEvent (curated articles)
- 30005 VideoCurationSetEvent (curated videos)
- 30006 PictureCurationSetEvent (curated pictures)
- 30007 KindMuteSetEvent (kind-specific mutes)
- 30015 InterestSetEvent (hashtag interest groups)
- 30063 ReleaseArtifactSetEvent (software release artifacts)
- 30267 AppCurationSetEvent (software app curation)
- 39092 MediaStarterPackEvent (media starter packs)

All events follow existing patterns with NIP-44 private tag
encryption, ALT tags, hint providers, and TagArrayBuilder extensions.
Registered all new kinds in EventFactory.

https://claude.ai/code/session_01QrYyt6KQepNGtRMDcCr5zM
This commit is contained in:
Claude
2026-04-02 04:01:03 +00:00
parent 0f7bcdba38
commit 662e870f2a
24 changed files with 2612 additions and 0 deletions
@@ -0,0 +1,156 @@
/*
* 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.nip51Lists.appCurationSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class AppCurationSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
AddressHintProvider {
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun apps() = tags.mapNotNull(AddressBookmark::parse)
companion object {
const val KIND = 30267
const val ALT = "App Curation Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: AppCurationSetEvent,
app: AddressBookmark,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): AppCurationSetEvent =
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(app.toTagArray()),
signer = signer,
createdAt = createdAt,
)
suspend fun remove(
earlierVersion: AppCurationSetEvent,
app: AddressBookmark,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): AppCurationSetEvent =
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.remove(app.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): AppCurationSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
title: String = "",
description: String? = null,
image: String? = null,
apps: List<AddressBookmark> = emptyList(),
dTag: String = Uuid.random().toString(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): AppCurationSetEvent {
val template =
build(title, apps, dTag, createdAt) {
if (description != null) description(description)
if (image != null) image(image)
}
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
fun build(
title: String = "",
apps: List<AddressBookmark> = emptyList(),
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AppCurationSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = "",
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(title)
apps(apps)
initializer()
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.nip51Lists.appCurationSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<AppCurationSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<AppCurationSetEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<AppCurationSetEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<AppCurationSetEvent>.apps(apps: List<AddressBookmark>) = addAll(apps.map { it.toTagArray() })
@@ -0,0 +1,205 @@
/*
* 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.nip51Lists.articleCurationSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class ArticleCurationSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider {
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun publicItems(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
suspend fun privateItems(signer: NostrSigner): List<BookmarkIdTag>? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse)
companion object {
const val KIND = 30004
const val ALT = "Article Curation Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: ArticleCurationSetEvent,
item: BookmarkIdTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ArticleCurationSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: ArticleCurationSetEvent,
item: BookmarkIdTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ArticleCurationSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
privateTags = privateTags.remove(item.toTagIdOnly()),
tags = earlierVersion.tags,
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.remove(item.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ArticleCurationSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
title: String = "",
description: String? = null,
image: String? = null,
publicItems: List<BookmarkIdTag> = emptyList(),
privateItems: List<BookmarkIdTag> = emptyList(),
dTag: String = Uuid.random().toString(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ArticleCurationSetEvent {
val template =
build(title, publicItems, privateItems, signer, dTag, createdAt) {
if (description != null) description(description)
if (image != null) image(image)
}
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun build(
title: String = "",
publicItems: List<BookmarkIdTag> = emptyList(),
privateItems: List<BookmarkIdTag> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<ArticleCurationSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateItems.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(title)
bookmarks(publicItems)
initializer()
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.nip51Lists.articleCurationSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<ArticleCurationSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<ArticleCurationSetEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<ArticleCurationSetEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<ArticleCurationSetEvent>.bookmarks(items: List<BookmarkIdTag>) = addAll(items.map { it.toTagArray() })
@@ -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.nip51Lists.gitAuthorList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class GitAuthorListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
fun publicAuthors() = tags.mapNotNull(UserTag::parse)
suspend fun privateAuthors(signer: NostrSigner) = privateTags(signer)?.mapNotNull(UserTag::parse)
companion object {
const val KIND = 10017
const val ALT = "Git Authors List"
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
suspend fun create(
publicAuthors: List<UserTag> = emptyList(),
privateAuthors: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitAuthorListEvent =
resign(
content = PrivateTagsInContent.encryptNip44(privateAuthors.map { it.toTagArray() }.toTypedArray(), signer),
tags = publicAuthors.map { it.toTagArray() }.toTypedArray(),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: GitAuthorListEvent,
author: UserTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitAuthorListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(author.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(author.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: GitAuthorListEvent,
author: UserTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitAuthorListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(author.toTagIdOnly()),
tags = earlierVersion.tags.remove(author.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitAuthorListEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
}
}
@@ -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.nip51Lists.gitRepositoryList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class GitRepositoryListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
AddressHintProvider {
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
fun publicRepositories() = tags.mapNotNull(AddressBookmark::parse)
suspend fun privateRepositories(signer: NostrSigner) = privateTags(signer)?.mapNotNull(AddressBookmark::parse)
companion object {
const val KIND = 10018
const val ALT = "Git Repositories List"
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
suspend fun create(
publicRepositories: List<AddressBookmark> = emptyList(),
privateRepositories: List<AddressBookmark> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitRepositoryListEvent =
resign(
content = PrivateTagsInContent.encryptNip44(privateRepositories.map { it.toTagArray() }.toTypedArray(), signer),
tags = publicRepositories.map { it.toTagArray() }.toTypedArray(),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: GitRepositoryListEvent,
repository: AddressBookmark,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitRepositoryListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(repository.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(repository.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: GitRepositoryListEvent,
repository: AddressBookmark,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitRepositoryListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(repository.toTagIdOnly()),
tags = earlierVersion.tags.remove(repository.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GitRepositoryListEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
}
}
@@ -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.nip51Lists.goodWikiAuthorList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class GoodWikiAuthorListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
fun publicAuthors() = tags.mapNotNull(UserTag::parse)
suspend fun privateAuthors(signer: NostrSigner) = privateTags(signer)?.mapNotNull(UserTag::parse)
companion object {
const val KIND = 10101
const val ALT = "Good Wiki Authors List"
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
suspend fun create(
publicAuthors: List<UserTag> = emptyList(),
privateAuthors: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiAuthorListEvent =
resign(
content = PrivateTagsInContent.encryptNip44(privateAuthors.map { it.toTagArray() }.toTypedArray(), signer),
tags = publicAuthors.map { it.toTagArray() }.toTypedArray(),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: GoodWikiAuthorListEvent,
author: UserTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiAuthorListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(author.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(author.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: GoodWikiAuthorListEvent,
author: UserTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiAuthorListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(author.toTagIdOnly()),
tags = earlierVersion.tags.remove(author.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiAuthorListEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
}
}
@@ -0,0 +1,141 @@
/*
* 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.nip51Lists.goodWikiRelayList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class GoodWikiRelayListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun publicRelays() = tags.relays()
suspend fun privateRelays(signer: NostrSigner) = privateTags(signer)?.relays()
suspend fun relays(signer: NostrSigner): List<NormalizedRelayUrl> = publicRelays() + (privateRelays(signer) ?: emptyList())
companion object {
const val KIND = 10102
const val ALT = "Good Wiki Relays List"
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
suspend fun create(
publicRelays: List<NormalizedRelayUrl> = emptyList(),
privateRelays: List<NormalizedRelayUrl> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiRelayListEvent =
resign(
content = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer),
tags = publicRelays.map { RelayTag.assemble(it) }.toTypedArray(),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: GoodWikiRelayListEvent,
relay: NormalizedRelayUrl,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiRelayListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(RelayTag.assemble(relay)),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(RelayTag.assemble(relay)),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: GoodWikiRelayListEvent,
relay: NormalizedRelayUrl,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiRelayListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val relayTag = RelayTag.assemble(relay)
return resign(
privateTags = privateTags.remove(relayTag),
tags = earlierVersion.tags.remove(relayTag),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): GoodWikiRelayListEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
}
}
@@ -0,0 +1,170 @@
/*
* 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.nip51Lists.interestSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
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.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.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class InterestSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun publicHashtags() = tags.mapNotNull(HashtagTag::parse)
suspend fun privateHashtags(signer: NostrSigner) = privateTags(signer)?.mapNotNull(HashtagTag::parse)
companion object {
const val KIND = 30015
const val ALT = "Interest Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: InterestSetEvent,
hashtag: String,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): InterestSetEvent {
val hashtagTag = HashtagTag.assemble(hashtag)
return if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.removeAny(listOf(hashtagTag)).plus(hashtagTag),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.removeAny(listOf(hashtagTag)).plus(hashtagTag),
signer = signer,
createdAt = createdAt,
)
}
}
suspend fun remove(
earlierVersion: InterestSetEvent,
hashtag: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): InterestSetEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.removeIgnoreCase(HashtagTag.assemble(hashtag)),
tags = earlierVersion.tags.removeIgnoreCase(HashtagTag.assemble(hashtag)),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): InterestSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
title: String = "",
publicHashtags: List<String> = emptyList(),
privateHashtags: List<String> = emptyList(),
dTag: String = Uuid.random().toString(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): InterestSetEvent {
val template = build(title, publicHashtags, privateHashtags, signer, dTag, createdAt)
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun build(
title: String = "",
publicHashtags: List<String> = emptyList(),
privateHashtags: List<String> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<InterestSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(title)
hashtags(publicHashtags)
initializer()
}
}
}
@@ -0,0 +1,29 @@
/*
* 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.nip51Lists.interestSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<InterestSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<InterestSetEvent>.hashtags(hashtags: List<String>) = addAll(hashtags.map { HashtagTag.assemble(it) })
@@ -0,0 +1,166 @@
/*
* 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.nip51Lists.kindMuteSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class KindMuteSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
fun publicMutedUsers() = tags.mapNotNull(UserTag::parse)
suspend fun privateMutedUsers(signer: NostrSigner) = privateTags(signer)?.mapNotNull(UserTag::parse)
companion object {
const val KIND = 30007
const val ALT = "Kind Mute Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: KindMuteSetEvent,
user: UserTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): KindMuteSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(user.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(user.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: KindMuteSetEvent,
user: UserTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): KindMuteSetEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(user.toTagIdOnly()),
tags = earlierVersion.tags.remove(user.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): KindMuteSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
suspend fun create(
kindNumber: Int,
publicMutedUsers: List<UserTag> = emptyList(),
privateMutedUsers: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): KindMuteSetEvent {
val template = build(kindNumber, publicMutedUsers, privateMutedUsers, signer, createdAt)
return signer.sign(template)
}
suspend fun build(
kindNumber: Int,
publicMutedUsers: List<UserTag> = emptyList(),
privateMutedUsers: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<KindMuteSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateMutedUsers.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(kindNumber.toString())
alt(ALT)
peoples(publicMutedUsers)
initializer()
}
}
}
@@ -0,0 +1,28 @@
/*
* 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.nip51Lists.kindMuteSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
fun TagArrayBuilder<KindMuteSetEvent>.peoples(people: List<UserTag>) = addAll(people.map { it.toTagArray() })
fun TagArrayBuilder<KindMuteSetEvent>.person(person: UserTag) = add(person.toTagArray())
@@ -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.nip51Lists.mediaFollowList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class MediaFollowListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
fun publicFollows() = tags.mapNotNull(UserTag::parse)
suspend fun privateFollows(signer: NostrSigner) = privateTags(signer)?.mapNotNull(UserTag::parse)
companion object {
const val KIND = 10020
const val ALT = "Media Follow List"
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
suspend fun create(
publicFollows: List<UserTag> = emptyList(),
privateFollows: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaFollowListEvent =
resign(
content = PrivateTagsInContent.encryptNip44(privateFollows.map { it.toTagArray() }.toTypedArray(), signer),
tags = publicFollows.map { it.toTagArray() }.toTypedArray(),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: MediaFollowListEvent,
follow: UserTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaFollowListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(follow.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(follow.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: MediaFollowListEvent,
follow: UserTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaFollowListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(follow.toTagIdOnly()),
tags = earlierVersion.tags.remove(follow.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaFollowListEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
}
}
@@ -0,0 +1,163 @@
/*
* 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.nip51Lists.mediaStarterPack
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
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.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.followList.followIdSet
import com.vitorpamplona.quartz.nip51Lists.followList.followIds
import com.vitorpamplona.quartz.nip51Lists.followList.follows
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class MediaStarterPackEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun follows() = tags.follows()
fun followIds() = tags.followIds()
fun followIdSet() = tags.followIdSet()
companion object {
const val KIND = 39092
const val ALT = "Media Starter Pack"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
name: String,
people: List<UserTag> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
): MediaStarterPackEvent {
val template = build(name, people, dTag, createdAt)
return signer.sign(template)
}
suspend fun addUsers(
earlierVersion: MediaStarterPackEvent,
people: List<UserTag>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaStarterPackEvent =
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(people.map { it.toTagArray() }),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: MediaStarterPackEvent,
person: UserTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = addUsers(earlierVersion, listOf(person), signer, createdAt)
suspend fun remove(
earlierVersion: MediaStarterPackEvent,
person: UserTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaStarterPackEvent =
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.remove(person.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): MediaStarterPackEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
fun build(
name: String,
people: List<UserTag> = emptyList(),
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<MediaStarterPackEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = "",
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(name)
people(people)
initializer()
}
}
}
@@ -0,0 +1,37 @@
/*
* 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.nip51Lists.mediaStarterPack
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<MediaStarterPackEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<MediaStarterPackEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<MediaStarterPackEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<MediaStarterPackEvent>.people(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() })
fun TagArrayBuilder<MediaStarterPackEvent>.person(person: UserTag) = add(person.toTagArray())
@@ -0,0 +1,198 @@
/*
* 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.nip51Lists.pictureCurationSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class PictureCurationSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider {
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun publicItems(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
suspend fun privateItems(signer: NostrSigner): List<BookmarkIdTag>? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse)
companion object {
const val KIND = 30006
const val ALT = "Picture Curation Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: PictureCurationSetEvent,
item: BookmarkIdTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PictureCurationSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: PictureCurationSetEvent,
item: BookmarkIdTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PictureCurationSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
privateTags = privateTags.remove(item.toTagIdOnly()),
tags = earlierVersion.tags,
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.remove(item.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PictureCurationSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
title: String = "",
description: String? = null,
image: String? = null,
publicItems: List<BookmarkIdTag> = emptyList(),
privateItems: List<BookmarkIdTag> = emptyList(),
dTag: String = Uuid.random().toString(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PictureCurationSetEvent {
val template =
build(title, publicItems, privateItems, signer, dTag, createdAt) {
if (description != null) description(description)
if (image != null) image(image)
}
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun build(
title: String = "",
publicItems: List<BookmarkIdTag> = emptyList(),
privateItems: List<BookmarkIdTag> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<PictureCurationSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateItems.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(title)
bookmarks(publicItems)
initializer()
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.nip51Lists.pictureCurationSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<PictureCurationSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<PictureCurationSetEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<PictureCurationSetEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<PictureCurationSetEvent>.bookmarks(items: List<BookmarkIdTag>) = addAll(items.map { it.toTagArray() })
@@ -0,0 +1,158 @@
/*
* 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.nip51Lists.releaseArtifactSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import com.vitorpamplona.quartz.nip51Lists.remove
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
@Immutable
class ReleaseArtifactSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider {
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun items(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
companion object {
const val KIND = 30063
const val ALT = "Release Artifact Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: ReleaseArtifactSetEvent,
item: BookmarkIdTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ReleaseArtifactSetEvent =
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
suspend fun remove(
earlierVersion: ReleaseArtifactSetEvent,
item: BookmarkIdTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ReleaseArtifactSetEvent =
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.remove(item.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ReleaseArtifactSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
title: String = "",
description: String? = null,
items: List<BookmarkIdTag> = emptyList(),
dTag: String = Uuid.random().toString(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ReleaseArtifactSetEvent {
val template =
build(title, items, dTag, createdAt) {
if (description != null) description(description)
}
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
fun build(
title: String = "",
items: List<BookmarkIdTag> = emptyList(),
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<ReleaseArtifactSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = "",
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(title)
artifacts(items)
initializer()
}
}
}
@@ -0,0 +1,32 @@
/*
* 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.nip51Lists.releaseArtifactSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<ReleaseArtifactSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<ReleaseArtifactSetEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<ReleaseArtifactSetEvent>.artifacts(items: List<BookmarkIdTag>) = addAll(items.map { it.toTagArray() })
@@ -0,0 +1,53 @@
/*
* 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.nip51Lists.simpleGroupList
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
class GroupTag(
val groupId: String,
val relayUrl: String,
val name: String? = null,
) {
fun toTagArray() = assemble(groupId, relayUrl, name)
fun toTagIdOnly() = arrayOf(TAG_NAME, groupId, relayUrl)
companion object {
const val TAG_NAME = "group"
fun parse(tag: Array<String>): GroupTag? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
ensure(tag[2].isNotEmpty()) { return null }
return GroupTag(tag[1], tag[2], tag.getOrNull(3))
}
fun assemble(
groupId: String,
relayUrl: String,
name: String? = null,
) = arrayOfNotNull(TAG_NAME, groupId, relayUrl, name)
}
}
@@ -0,0 +1,135 @@
/*
* 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.nip51Lists.simpleGroupList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class SimpleGroupListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun publicGroups() = tags.mapNotNull(GroupTag::parse)
suspend fun privateGroups(signer: NostrSigner) = privateTags(signer)?.mapNotNull(GroupTag::parse)
companion object {
const val KIND = 10009
const val ALT = "Simple Groups List"
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
suspend fun create(
publicGroups: List<GroupTag> = emptyList(),
privateGroups: List<GroupTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): SimpleGroupListEvent =
resign(
content = PrivateTagsInContent.encryptNip44(privateGroups.map { it.toTagArray() }.toTypedArray(), signer),
tags = publicGroups.map { it.toTagArray() }.toTypedArray(),
signer = signer,
createdAt = createdAt,
)
suspend fun add(
earlierVersion: SimpleGroupListEvent,
group: GroupTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): SimpleGroupListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(group.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(group.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: SimpleGroupListEvent,
group: GroupTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): SimpleGroupListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(group.toTagIdOnly()),
tags = earlierVersion.tags.remove(group.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): SimpleGroupListEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.nip51Lists.videoCurationSet
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<VideoCurationSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<VideoCurationSetEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<VideoCurationSetEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<VideoCurationSetEvent>.bookmarks(items: List<BookmarkIdTag>) = addAll(items.map { it.toTagArray() })
@@ -0,0 +1,205 @@
/*
* 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.nip51Lists.videoCurationSet
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class VideoCurationSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider {
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun publicItems(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
suspend fun privateItems(signer: NostrSigner): List<BookmarkIdTag>? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse)
companion object {
const val KIND = 30005
const val ALT = "Video Curation Set"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
suspend fun add(
earlierVersion: VideoCurationSetEvent,
item: BookmarkIdTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): VideoCurationSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
tags = earlierVersion.tags,
privateTags = privateTags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(item.toTagArray()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: VideoCurationSetEvent,
item: BookmarkIdTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): VideoCurationSetEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
privateTags = privateTags.remove(item.toTagIdOnly()),
tags = earlierVersion.tags,
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = earlierVersion.tags.remove(item.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
)
suspend fun resign(
content: String,
tags: TagArray,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): VideoCurationSetEvent {
val newTags =
if (tags.fastAny(AltTag::match)) {
tags
} else {
tags + AltTag.assemble(ALT)
}
return signer.sign(createdAt, KIND, newTags, content)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
title: String = "",
description: String? = null,
image: String? = null,
publicItems: List<BookmarkIdTag> = emptyList(),
privateItems: List<BookmarkIdTag> = emptyList(),
dTag: String = Uuid.random().toString(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): VideoCurationSetEvent {
val template =
build(title, publicItems, privateItems, signer, dTag, createdAt) {
if (description != null) description(description)
if (image != null) image(image)
}
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun build(
title: String = "",
publicItems: List<BookmarkIdTag> = emptyList(),
privateItems: List<BookmarkIdTag> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<VideoCurationSetEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateItems.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
title(title)
bookmarks(publicItems)
initializer()
}
}
}
@@ -111,14 +111,25 @@ import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent
import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent
import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent
import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent
import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent
import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent
import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
@@ -126,6 +137,9 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent
import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent
import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
@@ -271,6 +285,7 @@ class EventFactory {
when (kind) {
AcceptedBadgeSetEvent.KIND -> AcceptedBadgeSetEvent(id, pubKey, createdAt, tags, content, sig)
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppCurationSetEvent.KIND -> AppCurationSetEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.KIND -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AppSpecificDataEvent.KIND -> AppSpecificDataEvent(id, pubKey, createdAt, tags, content, sig)
@@ -278,6 +293,7 @@ class EventFactory {
AttestationRequestEvent.KIND -> AttestationRequestEvent(id, pubKey, createdAt, tags, content, sig)
AttestorRecommendationEvent.KIND -> AttestorRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AttestorProficiencyEvent.KIND -> AttestorProficiencyEvent(id, pubKey, createdAt, tags, content, sig)
ArticleCurationSetEvent.KIND -> ArticleCurationSetEvent(id, pubKey, createdAt, tags, content, sig)
AudioHeaderEvent.KIND -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
AuctionEvent.KIND -> AuctionEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
@@ -356,19 +372,25 @@ class EventFactory {
GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GeohashListEvent.KIND -> GeohashListEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
GitAuthorListEvent.KIND -> GitAuthorListEvent(id, pubKey, createdAt, tags, content, sig)
GitRepositoryListEvent.KIND -> GitRepositoryListEvent(id, pubKey, createdAt, tags, content, sig)
GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig)
GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig)
GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig)
GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig)
GoodWikiAuthorListEvent.KIND -> GoodWikiAuthorListEvent(id, pubKey, createdAt, tags, content, sig)
GoodWikiRelayListEvent.KIND -> GoodWikiRelayListEvent(id, pubKey, createdAt, tags, content, sig)
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
HashtagListEvent.KIND -> HashtagListEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
IndexerRelayListEvent.KIND -> IndexerRelayListEvent(id, pubKey, createdAt, tags, content, sig)
InterestSetEvent.KIND -> InterestSetEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig)
LabelEvent.KIND -> LabelEvent(id, pubKey, createdAt, tags, content, sig)
KindMuteSetEvent.KIND -> KindMuteSetEvent(id, pubKey, createdAt, tags, content, sig)
LabeledBookmarkListEvent.KIND -> LabeledBookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
@@ -386,6 +408,8 @@ class EventFactory {
MeetingRoomPresenceEvent.KIND -> MeetingRoomPresenceEvent(id, pubKey, createdAt, tags, content, sig)
MeetingSpaceEvent.KIND -> MeetingSpaceEvent(id, pubKey, createdAt, tags, content, sig)
MintRecommendationEvent.KIND -> MintRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
MediaFollowListEvent.KIND -> MediaFollowListEvent(id, pubKey, createdAt, tags, content, sig)
MediaStarterPackEvent.KIND -> MediaStarterPackEvent(id, pubKey, createdAt, tags, content, sig)
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
NamedSiteEvent.KIND -> NamedSiteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -437,6 +461,7 @@ class EventFactory {
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
PaymentTargetsEvent.KIND -> PaymentTargetsEvent(id, pubKey, createdAt, tags, content, sig)
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
PictureCurationSetEvent.KIND -> PictureCurationSetEvent(id, pubKey, createdAt, tags, content, sig)
P2POrderEvent.KIND -> P2POrderEvent(id, pubKey, createdAt, tags, content, sig)
PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
@@ -463,6 +488,7 @@ class EventFactory {
RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig)
RelayDiscoveryEvent.KIND -> RelayDiscoveryEvent(id, pubKey, createdAt, tags, content, sig)
RelayMonitorEvent.KIND -> RelayMonitorEvent(id, pubKey, createdAt, tags, content, sig)
ReleaseArtifactSetEvent.KIND -> ReleaseArtifactSetEvent(id, pubKey, createdAt, tags, content, sig)
RelaySetEvent.KIND -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
ReportEvent.KIND -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RootSiteEvent.KIND -> RootSiteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -470,6 +496,7 @@ class EventFactory {
RequestToVanishEvent.KIND -> RequestToVanishEvent(id, pubKey, createdAt, tags, content, sig)
SealedRumorEvent.KIND -> SealedRumorEvent(id, pubKey, createdAt, tags, content, sig)
SearchRelayListEvent.KIND -> SearchRelayListEvent(id, pubKey, createdAt, tags, content, sig)
SimpleGroupListEvent.KIND -> SimpleGroupListEvent(id, pubKey, createdAt, tags, content, sig)
StallEvent.KIND -> StallEvent(id, pubKey, createdAt, tags, content, sig)
StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -480,6 +507,7 @@ class EventFactory {
TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig)
TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
TrustProviderListEvent.KIND -> TrustProviderListEvent(id, pubKey, createdAt, tags, content, sig)
VideoCurationSetEvent.KIND -> VideoCurationSetEvent(id, pubKey, createdAt, tags, content, sig)
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
VideoNormalEvent.KIND -> VideoNormalEvent(id, pubKey, createdAt, tags, content, sig)