Merge branch 'vitorpamplona:main' into profiles-list-management

This commit is contained in:
KotlinGeekDev
2025-08-14 04:22:54 +00:00
committed by GitHub
78 changed files with 941 additions and 820 deletions
@@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
@@ -216,7 +216,7 @@ class EventFactory {
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
DraftWrapEvent.KIND -> DraftWrapEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
@@ -206,7 +206,7 @@ class NostrClient(
return false
}
fun sendRequest(
fun openReqSubscription(
subId: String = newSubId(),
filters: Map<NormalizedRelayUrl, List<Filter>>,
) {
@@ -239,7 +239,7 @@ class NostrClient(
}
}
fun sendCount(
fun openCountSubscription(
subId: String = newSubId(),
filters: Map<NormalizedRelayUrl, List<Filter>>,
) {
@@ -49,7 +49,6 @@ fun NostrClient.downloadFirstEvent(
if (subId == subscriptionId) {
unsubscribe(this)
close(subscriptionId)
onResponse(event)
}
}
@@ -57,10 +56,12 @@ fun NostrClient.downloadFirstEvent(
subscribe(listener)
sendRequest(subscriptionId, filters)
openReqSubscription(subscriptionId, filters)
GlobalScope.launch(Dispatchers.IO) {
delay(30000)
close(subscriptionId)
unsubscribe(listener)
}
}
@@ -126,14 +126,14 @@ class SubscriptionController(
// was active and is not active anymore, just close.
client.close(subId)
} else {
client.sendRequest(subId, updatedFilters)
client.openReqSubscription(subId, updatedFilters)
}
} else {
if (updatedFilters == null) {
// was not active and is still not active, does nothing
} else {
// was not active and becomes active, sends the entire filter.
client.sendRequest(subId, updatedFilters)
client.openReqSubscription(subId, updatedFilters)
}
}
}
@@ -50,7 +50,7 @@ class ReqCmd(
}
@JvmStatic
fun parse(msgArray: JsonNode): com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd {
fun parse(msgArray: JsonNode): ReqCmd {
val filters = mutableListOf<Filter>()
for (i in 2 until msgArray.size()) {
@@ -60,7 +60,7 @@ class ReqCmd(
}
}
return com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd(
return ReqCmd(
msgArray.get(1).asText(),
filters,
)
@@ -30,6 +30,7 @@ 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.events.EventReference
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
@@ -121,16 +122,37 @@ class PrivateDmEvent(
}
}
fun build(
to: PTag,
encryptedMessage: String,
suspend fun build(
toUser: PTag,
message: String,
imetas: List<IMetaTag>? = null,
replyingTo: EventReference? = null,
createdAt: Long = TimeUtils.now(),
signer: NostrSigner,
initializer: TagArrayBuilder<PrivateDmEvent>.() -> Unit = {},
) = eventTemplate(KIND, encryptedMessage, createdAt) {
) = eventTemplate(
kind = KIND,
description =
signer.nip04Encrypt(
prepareMessageToEncrypt(message, imetas),
toUser.pubKey,
),
createdAt = createdAt,
) {
alt(ALT)
pTag(to)
pTag(toUser)
replyingTo?.let { reply(it) }
initializer()
}
suspend fun create(
to: PTag,
message: String,
imetas: List<IMetaTag>? = null,
replyingTo: EventReference?,
createdAt: Long = TimeUtils.now(),
signer: NostrSigner,
) = signer.sign(build(to, message, imetas, replyingTo, createdAt, signer))
}
}
@@ -35,11 +35,11 @@ class DraftBuilder {
draft: T,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
): DraftWrapEvent {
val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey)
val template =
eventTemplate<DraftEvent>(DraftEvent.KIND, encryptedContent, createdAt) {
alt(DraftEvent.ALT_DESCRIPTION)
eventTemplate<DraftWrapEvent>(DraftWrapEvent.KIND, encryptedContent, createdAt) {
alt(DraftWrapEvent.ALT_DESCRIPTION)
dTag(dTag)
kind(draft.kind)
@@ -1,230 +0,0 @@
/**
* 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.nip37Drafts
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.core.JsonParseException
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
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.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class DraftEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
override fun isContentEncoded() = true
fun isDeleted() = content == ""
fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey
suspend fun createDeletedEvent(signer: NostrSigner): DraftEvent = signer.sign(createdAt, KIND, tags, "")
suspend fun decryptInnerEvent(signer: NostrSigner): Event {
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
val json = signer.nip44Decrypt(content, pubKey)
return try {
fromJson(json)
} catch (e: JsonParseException) {
Log.w("DraftEvent", "Unable to parse inner event of a draft: $json")
throw e
}
}
companion object {
const val KIND = 31234
const val ALT_DESCRIPTION = "Draft Event"
fun createAddressTag(
pubKey: HexKey,
dTag: String,
): String = Address.assemble(KIND, pubKey, dTag)
@Suppress("DEPRECATION")
suspend fun create(
dTag: String,
originalNote: TorrentCommentEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tagsWithMarkers =
originalNote.tags.filter {
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
}
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: InteractiveStoryBaseEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
return create(dTag, originalNote, tags, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: LiveActivitiesChatMessageEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag(), "", "root")) }
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it, "", "reply")) }
return create(dTag, originalNote, tags, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: ChannelMessageEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
originalNote.channelId()?.let { tags.add(arrayOf("e", it)) }
return create(dTag, originalNote, tags, signer, createdAt)
}
@Suppress("DEPRECATION")
suspend fun create(
dTag: String,
originalNote: GitReplyEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) }
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
return create(dTag, originalNote, tags, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: PollNoteEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tagsWithMarkers =
originalNote.tags.filter {
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
}
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: CommentEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tagsWithMarkers = originalNote.rootScopes() + originalNote.directReplies()
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: TextNoteEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tagsWithMarkers =
originalNote.tags.filter {
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
}
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
}
suspend fun create(
dTag: String,
innerEvent: Event,
anchorTagArray: List<Array<String>> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("d", dTag))
tags.add(arrayOf("k", "${innerEvent.kind}"))
if (anchorTagArray.isNotEmpty()) {
tags.addAll(anchorTagArray)
}
val draft =
signer.sign<DraftEvent>(
createdAt = createdAt,
kind = KIND,
tags = tags.toTypedArray(),
content = signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey),
)
return draft
}
}
}
@@ -29,8 +29,8 @@ class DraftEventCache(
signer: NostrSigner,
) {
private val decryptionCache =
object : LruCache<DraftEvent, DraftEventDecryptCache>(1000) {
override fun create(key: DraftEvent): DraftEventDecryptCache? =
object : LruCache<DraftWrapEvent, DraftEventDecryptCache>(1000) {
override fun create(key: DraftWrapEvent): DraftEventDecryptCache? =
if (!key.isDeleted() && key.pubKey == signer.pubKey) {
DraftEventDecryptCache(signer)
} else {
@@ -38,23 +38,23 @@ class DraftEventCache(
}
}
fun delete(event: DraftEvent) = decryptionCache.remove(event)
fun delete(event: DraftWrapEvent) = decryptionCache.remove(event)
fun preload(
event: DraftEvent,
event: DraftWrapEvent,
result: Event,
) = decryptionCache[event]?.preload(result)
fun preCachedDraft(event: DraftEvent): Event? = decryptionCache[event]?.cached()
fun preCachedDraft(event: DraftWrapEvent): Event? = decryptionCache[event]?.cached()
suspend fun cachedDraft(event: DraftEvent) = decryptionCache[event]?.decrypt(event)
suspend fun cachedDraft(event: DraftWrapEvent) = decryptionCache[event]?.decrypt(event)
}
class DraftEventDecryptCache(
signer: NostrSigner,
) : DecryptCache<DraftEvent, Event>(signer) {
) : DecryptCache<DraftWrapEvent, Event>(signer) {
override suspend fun decryptAndParse(
event: DraftEvent,
event: DraftWrapEvent,
signer: NostrSigner,
): Event = event.decryptInnerEvent(signer)
}
@@ -0,0 +1,125 @@
/**
* 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.nip37Drafts
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.core.JsonParseException
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class DraftWrapEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
override fun isContentEncoded() = true
fun isDeleted() = content == ""
fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey
suspend fun decryptInnerEvent(signer: NostrSigner): Event {
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
val json = signer.nip44Decrypt(content, pubKey)
return try {
fromJson(json)
} catch (e: JsonParseException) {
Log.w("DraftEvent", "Unable to parse inner event of a draft: $json")
throw e
}
}
companion object {
const val KIND = 31234
const val ALT_DESCRIPTION = "Draft Event"
fun createAddress(
pubKey: HexKey,
dTag: String,
): Address = Address(KIND, pubKey, dTag)
fun createAddressTag(
pubKey: HexKey,
dTag: String,
): String = Address.assemble(KIND, pubKey, dTag)
suspend fun build(
dTag: String,
draft: Event,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<DraftWrapEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = signer.nip44Encrypt(draft.toJson(), signer.pubKey),
createdAt = createdAt,
) {
alt(ALT_DESCRIPTION)
dTag(dTag)
kind(draft.kind)
initializer()
}
suspend fun buildDeleted(
dTag: String,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<DraftWrapEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = "",
createdAt = createdAt,
) {
alt(ALT_DESCRIPTION)
dTag(dTag)
initializer()
}
suspend fun create(
dTag: String,
draft: Event,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = signer.sign(build(dTag, draft, signer, createdAt))
suspend fun createDeletedEvent(
dTag: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftWrapEvent = signer.sign(buildDeleted(dTag, createdAt))
}
}
@@ -76,8 +76,11 @@ class AdvertisedRelayInfo(
@JvmStatic
fun parseReadNorm(tag: Array<String>): NormalizedRelayUrl? {
ensure(match(tag)) { return null }
ensure(AdvertisedRelayType.isRead(tag.getOrNull(2))) { return null }
ensure(tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()) { return null }
if (tag.has(2)) {
ensure(AdvertisedRelayType.isRead(tag[2])) { return null }
}
val relay = RelayUrlNormalizer.normalizeOrNull(tag[1])
@@ -88,8 +91,11 @@ class AdvertisedRelayInfo(
@JvmStatic
fun parseWriteNorm(tag: Array<String>): NormalizedRelayUrl? {
ensure(match(tag)) { return null }
ensure(AdvertisedRelayType.isWrite(tag.getOrNull(2))) { return null }
ensure(tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()) { return null }
if (tag.has(2)) {
ensure(AdvertisedRelayType.isWrite(tag[2])) { return null }
}
val relay = RelayUrlNormalizer.normalizeOrNull(tag[1])