From 662e870f2a2cffcd710c0d8f8279eaa02e64e5ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 04:01:03 +0000 Subject: [PATCH] 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 --- .../appCurationSet/AppCurationSetEvent.kt | 156 +++++++++++++ .../appCurationSet/TagArrayBuilderExt.kt | 35 +++ .../ArticleCurationSetEvent.kt | 205 ++++++++++++++++++ .../articleCurationSet/TagArrayBuilderExt.kt | 35 +++ .../gitAuthorList/GitAuthorListEvent.kt | 142 ++++++++++++ .../GitRepositoryListEvent.kt | 142 ++++++++++++ .../GoodWikiAuthorListEvent.kt | 142 ++++++++++++ .../GoodWikiRelayListEvent.kt | 141 ++++++++++++ .../interestSet/InterestSetEvent.kt | 170 +++++++++++++++ .../interestSet/TagArrayBuilderExt.kt | 29 +++ .../kindMuteSet/KindMuteSetEvent.kt | 166 ++++++++++++++ .../kindMuteSet/TagArrayBuilderExt.kt | 28 +++ .../mediaFollowList/MediaFollowListEvent.kt | 142 ++++++++++++ .../mediaStarterPack/MediaStarterPackEvent.kt | 163 ++++++++++++++ .../mediaStarterPack/TagArrayBuilderExt.kt | 37 ++++ .../PictureCurationSetEvent.kt | 198 +++++++++++++++++ .../pictureCurationSet/TagArrayBuilderExt.kt | 35 +++ .../ReleaseArtifactSetEvent.kt | 158 ++++++++++++++ .../releaseArtifactSet/TagArrayBuilderExt.kt | 32 +++ .../nip51Lists/simpleGroupList/GroupTag.kt | 53 +++++ .../simpleGroupList/SimpleGroupListEvent.kt | 135 ++++++++++++ .../videoCurationSet/TagArrayBuilderExt.kt | 35 +++ .../videoCurationSet/VideoCurationSetEvent.kt | 205 ++++++++++++++++++ .../quartz/utils/EventFactory.kt | 28 +++ 24 files changed, 2612 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/AppCurationSetEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/ArticleCurationSetEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitRepositoryList/GitRepositoryListEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiAuthorList/GoodWikiAuthorListEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiRelayList/GoodWikiRelayListEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/InterestSetEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/KindMuteSetEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaFollowList/MediaFollowListEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/PictureCurationSetEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/GroupTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/SimpleGroupListEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/VideoCurationSetEvent.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/AppCurationSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/AppCurationSetEvent.kt new file mode 100644 index 000000000..7b82153cc --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/AppCurationSetEvent.kt @@ -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>, + 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 = 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 = emptyList(), + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = "", + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + title(title) + apps(apps) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..a9217bb55 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/appCurationSet/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.apps(apps: List) = addAll(apps.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/ArticleCurationSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/ArticleCurationSetEvent.kt new file mode 100644 index 000000000..d5cab8226 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/ArticleCurationSetEvent.kt @@ -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>, + 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 = tags.mapNotNull(BookmarkIdTag::parse) + + suspend fun privateItems(signer: NostrSigner): List? = 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 = emptyList(), + privateItems: List = 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 = emptyList(), + privateItems: List = emptyList(), + signer: NostrSigner, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateItems.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + title(title) + bookmarks(publicItems) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..d484ead36 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/articleCurationSet/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.bookmarks(items: List) = addAll(items.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt new file mode 100644 index 000000000..14e117603 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitAuthorList/GitAuthorListEvent.kt @@ -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>, + 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 = emptyList(), + privateAuthors: List = 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitRepositoryList/GitRepositoryListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitRepositoryList/GitRepositoryListEvent.kt new file mode 100644 index 000000000..0cb4ee622 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/gitRepositoryList/GitRepositoryListEvent.kt @@ -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>, + 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 = emptyList(), + privateRepositories: List = 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiAuthorList/GoodWikiAuthorListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiAuthorList/GoodWikiAuthorListEvent.kt new file mode 100644 index 000000000..9146d22ad --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiAuthorList/GoodWikiAuthorListEvent.kt @@ -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>, + 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 = emptyList(), + privateAuthors: List = 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiRelayList/GoodWikiRelayListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiRelayList/GoodWikiRelayListEvent.kt new file mode 100644 index 000000000..536937ff4 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/goodWikiRelayList/GoodWikiRelayListEvent.kt @@ -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>, + 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 = 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 = emptyList(), + privateRelays: List = 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/InterestSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/InterestSetEvent.kt new file mode 100644 index 000000000..3b8c956ff --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/InterestSetEvent.kt @@ -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>, + 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 = emptyList(), + privateHashtags: List = 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 = emptyList(), + privateHashtags: List = emptyList(), + signer: NostrSigner, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> 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() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..b092f7505 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestSet/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.hashtags(hashtags: List) = addAll(hashtags.map { HashtagTag.assemble(it) }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/KindMuteSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/KindMuteSetEvent.kt new file mode 100644 index 000000000..6d231bb8a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/KindMuteSetEvent.kt @@ -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>, + 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 = emptyList(), + privateMutedUsers: List = 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 = emptyList(), + privateMutedUsers: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateMutedUsers.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + dTag(kindNumber.toString()) + alt(ALT) + peoples(publicMutedUsers) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..7559081b3 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/kindMuteSet/TagArrayBuilderExt.kt @@ -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.peoples(people: List) = addAll(people.map { it.toTagArray() }) + +fun TagArrayBuilder.person(person: UserTag) = add(person.toTagArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaFollowList/MediaFollowListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaFollowList/MediaFollowListEvent.kt new file mode 100644 index 000000000..60c0d6ead --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaFollowList/MediaFollowListEvent.kt @@ -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>, + 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 = emptyList(), + privateFollows: List = 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt new file mode 100644 index 000000000..03bb69a03 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt @@ -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>, + 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 = 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, + 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>, + 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 = emptyList(), + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = "", + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + title(name) + people(people) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/TagArrayBuilderExt.kt new file mode 100644 index 000000000..fa52bfe35 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.people(peoples: List) = addAll(peoples.map { it.toTagArray() }) + +fun TagArrayBuilder.person(person: UserTag) = add(person.toTagArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/PictureCurationSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/PictureCurationSetEvent.kt new file mode 100644 index 000000000..b93dc207e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/PictureCurationSetEvent.kt @@ -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>, + 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 = tags.mapNotNull(BookmarkIdTag::parse) + + suspend fun privateItems(signer: NostrSigner): List? = 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 = emptyList(), + privateItems: List = 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 = emptyList(), + privateItems: List = emptyList(), + signer: NostrSigner, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateItems.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + title(title) + bookmarks(publicItems) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..b06a011f8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/pictureCurationSet/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.bookmarks(items: List) = addAll(items.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt new file mode 100644 index 000000000..48af933fe --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt @@ -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>, + 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 = 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 = 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 = emptyList(), + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = "", + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + title(title) + artifacts(items) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..688a365d4 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) + +fun TagArrayBuilder.artifacts(items: List) = addAll(items.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/GroupTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/GroupTag.kt new file mode 100644 index 000000000..b3e860bdd --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/GroupTag.kt @@ -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): 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) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/SimpleGroupListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/SimpleGroupListEvent.kt new file mode 100644 index 000000000..404720d28 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/simpleGroupList/SimpleGroupListEvent.kt @@ -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>, + 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 = emptyList(), + privateGroups: List = 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/TagArrayBuilderExt.kt new file mode 100644 index 000000000..33b3bb66d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/TagArrayBuilderExt.kt @@ -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.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.bookmarks(items: List) = addAll(items.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/VideoCurationSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/VideoCurationSetEvent.kt new file mode 100644 index 000000000..26501390a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/videoCurationSet/VideoCurationSetEvent.kt @@ -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>, + 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 = tags.mapNotNull(BookmarkIdTag::parse) + + suspend fun privateItems(signer: NostrSigner): List? = 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 = emptyList(), + privateItems: List = 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 = emptyList(), + privateItems: List = emptyList(), + signer: NostrSigner, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateItems.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + title(title) + bookmarks(publicItems) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index 06b51ad2e..507364ad6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -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)