Moves NIP-55 calls to be suspending functions.
Moves NIP-55 calls to include an ID per call, not per event. Adds error handling facilities to the Signer functions. Moves the indexing of the decrypted objects to outside the LocalCache Migrates Signers to become suspending functions. Migrates Decryption caching systems to outside the Events themselves. Migrates all NIP-51 lists to the new structure. Migrates Drafts and NIP-04 and NIP-17 DMs to the new structure Migrates Bookmarks to the new structure. Changes the Room route to avoid using hashcode.
This commit is contained in:
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -74,17 +75,13 @@ class OtsTest {
|
||||
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
|
||||
var newOts: OtsEvent? = null
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
signer.sign(OtsEvent.build(eventId, upgraded!!)) {
|
||||
newOts = it
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
val newOts = runBlocking { signer.sign(OtsEvent.build(eventId, upgraded!!)) }
|
||||
|
||||
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
|
||||
println(newOts!!.toJson())
|
||||
println(newOts.toJson())
|
||||
println(resolver.info(newOts.otsByteArray()))
|
||||
|
||||
assertEquals(1708879025L, newOts.verify(resolver))
|
||||
@@ -93,18 +90,17 @@ class OtsTest {
|
||||
@Test
|
||||
fun createOTSEventAndVerify() {
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
var ots: OtsEvent? = null
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver))) {
|
||||
ots = it
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
val ots =
|
||||
runBlocking {
|
||||
signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver)))
|
||||
}
|
||||
|
||||
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
|
||||
println(ots!!.toJson())
|
||||
println(ots.toJson())
|
||||
println(resolver.info(ots.otsByteArray()))
|
||||
|
||||
assertEquals(null, ots.verify(resolver))
|
||||
|
||||
+149
-150
@@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class Nip46Test {
|
||||
@@ -49,206 +49,205 @@ internal class Nip46Test {
|
||||
sig = "",
|
||||
)
|
||||
|
||||
fun <T : BunkerMessage> encodeDecodeEvent(req: T): T {
|
||||
var countDownLatch = CountDownLatch(1)
|
||||
var eventStr: String? = null
|
||||
suspend fun <T : BunkerMessage> encodeDecodeEvent(req: T): T {
|
||||
val eventStr = NostrConnectEvent.create(req, remoteKey.pubKey, signer).toJson()
|
||||
|
||||
NostrConnectEvent.create(req, remoteKey.pubKey, signer) {
|
||||
eventStr = it.toJson()
|
||||
countDownLatch.countDown()
|
||||
return (Event.fromJson(eventStr) as NostrConnectEvent).decryptMessage(signer) as T
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signEncoder() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestSign(event = dummyEvent)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(BunkerRequestSign.METHOD_NAME, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(dummyEvent.id, actual.event.id)
|
||||
}
|
||||
|
||||
countDownLatch.await()
|
||||
@Test
|
||||
fun connectEncoder() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestConnect(remoteKey = remoteKey.pubKey)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
countDownLatch = CountDownLatch(1)
|
||||
var innerMessage: T? = null
|
||||
|
||||
(Event.fromJson(eventStr!!) as NostrConnectEvent).plainContent(signer) {
|
||||
innerMessage = it as T
|
||||
countDownLatch.countDown()
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
|
||||
countDownLatch.await()
|
||||
@Test
|
||||
fun pingEncoder() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestPing()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
return innerMessage!!
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signEncoder() {
|
||||
val expected = BunkerRequestSign(event = dummyEvent)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun getPubkeyEncoder() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestGetPublicKey()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(BunkerRequestSign.METHOD_NAME, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(dummyEvent.id, actual.event.id)
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun connectEncoder() {
|
||||
val expected = BunkerRequestConnect(remoteKey = remoteKey.pubKey)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun getRelaysEncoder() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestGetRelays()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pingEncoder() {
|
||||
val expected = BunkerRequestPing()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testNip04Encrypt() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestNip04Encrypt(pubKey = peer.pubKey, message = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.message, actual.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getPubkeyEncoder() {
|
||||
val expected = BunkerRequestGetPublicKey()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testNip44Encrypt() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestNip44Encrypt(pubKey = peer.pubKey, message = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.message, actual.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelaysEncoder() {
|
||||
val expected = BunkerRequestGetRelays()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testNip04Decrypt() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestNip04Decrypt(pubKey = peer.pubKey, ciphertext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.ciphertext, actual.ciphertext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNip04Encrypt() {
|
||||
val expected = BunkerRequestNip04Encrypt(pubKey = peer.pubKey, message = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testNip44Decrypt() =
|
||||
runBlocking {
|
||||
val expected = BunkerRequestNip44Decrypt(pubKey = peer.pubKey, ciphertext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.message, actual.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNip44Encrypt() {
|
||||
val expected = BunkerRequestNip44Encrypt(pubKey = peer.pubKey, message = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.message, actual.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNip04Decrypt() {
|
||||
val expected = BunkerRequestNip04Decrypt(pubKey = peer.pubKey, ciphertext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.ciphertext, actual.ciphertext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNip44Decrypt() {
|
||||
val expected = BunkerRequestNip44Decrypt(pubKey = peer.pubKey, ciphertext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.ciphertext, actual.ciphertext)
|
||||
}
|
||||
assertEquals(expected.method, actual.method)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.pubKey, actual.pubKey)
|
||||
assertEquals(expected.ciphertext, actual.ciphertext)
|
||||
}
|
||||
|
||||
// Responses
|
||||
|
||||
@Test
|
||||
fun testAckResponse() {
|
||||
val expected = BunkerResponseAck()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testAckResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponseAck()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPongResponse() {
|
||||
val expected = BunkerResponsePong()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testPongResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponsePong()
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorResponse() {
|
||||
val expected = BunkerResponseError(error = "Error")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testErrorResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponseError(error = "Error")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEventResponse() {
|
||||
val expected = BunkerResponseEvent(event = dummyEvent)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testEventResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponseEvent(event = dummyEvent)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(dummyEvent.id, actual.event.id)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(dummyEvent.id, actual.event.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPubkeyResponse() {
|
||||
val expected = BunkerResponsePublicKey(pubkey = peer.pubKey)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testPubkeyResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponsePublicKey(pubkey = peer.pubKey)
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.pubkey, actual.pubkey)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.pubkey, actual.pubkey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRelaysResponse() {
|
||||
val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false)))
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testRelaysResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false)))
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.relays["url"], actual.relays["url"])
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.relays["url"], actual.relays["url"])
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Impossible to recreate the class since there are no hints on the json")
|
||||
fun testDecryptResponse() {
|
||||
val expected = BunkerResponseDecrypt(plaintext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testDecryptResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponseDecrypt(plaintext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.plaintext, actual.plaintext)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.plaintext, actual.plaintext)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Impossible to recreate the class since there are no hints on the json")
|
||||
fun testEncryptResponse() {
|
||||
val expected = BunkerResponseEncrypt(ciphertext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
fun testEncryptResponse() =
|
||||
runBlocking {
|
||||
val expected = BunkerResponseEncrypt(ciphertext = "Test")
|
||||
val actual = encodeDecodeEvent(expected)
|
||||
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.ciphertext, actual.ciphertext)
|
||||
}
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.result, actual.result)
|
||||
assertEquals(expected.error, actual.error)
|
||||
assertEquals(expected.ciphertext, actual.ciphertext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEncryption.Companion.createEncryptionPrivateKey
|
||||
import com.vitorpamplona.quartz.nip59GiftWraps.wait1SecondForResult
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -89,42 +89,35 @@ class PrivateZapTests {
|
||||
),
|
||||
)
|
||||
|
||||
var resultPrivateZap: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
LnZapRequestEvent.create(
|
||||
originalNote = poll,
|
||||
relays = setOf("wss://relay.damus.io/"),
|
||||
signer = loggedIn,
|
||||
pollOption = 0,
|
||||
message = "",
|
||||
zapType = LnZapEvent.ZapType.PRIVATE,
|
||||
toUserPubHex = null,
|
||||
) { privateZapRequest ->
|
||||
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
|
||||
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
|
||||
|
||||
if (recepientPK != null && recepientPost != null) {
|
||||
val privateKey =
|
||||
createEncryptionPrivateKey(
|
||||
loggedIn.keyPair.privKey!!.toHexKey(),
|
||||
recepientPost,
|
||||
privateZapRequest.createdAt,
|
||||
)
|
||||
val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
|
||||
|
||||
println(decodedPrivateZap?.toJson())
|
||||
|
||||
resultPrivateZap = decodedPrivateZap
|
||||
|
||||
onDone()
|
||||
} else {
|
||||
fail("Should not be null")
|
||||
}
|
||||
val privateZapRequest =
|
||||
runBlocking {
|
||||
LnZapRequestEvent.create(
|
||||
originalNote = poll,
|
||||
relays = setOf("wss://relay.damus.io/"),
|
||||
signer = loggedIn,
|
||||
pollOption = 0,
|
||||
message = "",
|
||||
zapType = LnZapEvent.ZapType.PRIVATE,
|
||||
toUserPubHex = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(resultPrivateZap)
|
||||
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
|
||||
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
|
||||
|
||||
if (recepientPK != null && recepientPost != null) {
|
||||
val privateKey =
|
||||
createEncryptionPrivateKey(
|
||||
loggedIn.keyPair.privKey!!.toHexKey(),
|
||||
recepientPost,
|
||||
privateZapRequest.createdAt,
|
||||
)
|
||||
val decodedPrivateZap = PrivateZapRequestBuilder().decryptAnonTag(privateZapRequest.getAnonTag(), privateKey, recepientPK)
|
||||
|
||||
assertNotNull(decodedPrivateZap)
|
||||
} else {
|
||||
fail("Should not be null")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,41 +149,34 @@ class PrivateZapTests {
|
||||
KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")),
|
||||
)
|
||||
|
||||
var resultPrivateZap: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
LnZapRequestEvent.create(
|
||||
originalNote = textNote,
|
||||
relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"),
|
||||
signer = loggedIn,
|
||||
pollOption = null,
|
||||
message = "test",
|
||||
zapType = LnZapEvent.ZapType.PRIVATE,
|
||||
toUserPubHex = null,
|
||||
) { privateZapRequest ->
|
||||
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
|
||||
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
|
||||
|
||||
if (recepientPK != null && recepientPost != null) {
|
||||
val privateKey =
|
||||
createEncryptionPrivateKey(
|
||||
loggedIn.keyPair.privKey!!.toHexKey(),
|
||||
recepientPost,
|
||||
privateZapRequest.createdAt,
|
||||
)
|
||||
val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
|
||||
|
||||
println(decodedPrivateZap?.toJson())
|
||||
|
||||
resultPrivateZap = decodedPrivateZap
|
||||
|
||||
onDone()
|
||||
} else {
|
||||
fail("Should not be null")
|
||||
}
|
||||
val privateZapRequest =
|
||||
runBlocking {
|
||||
LnZapRequestEvent.create(
|
||||
originalNote = textNote,
|
||||
relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"),
|
||||
signer = loggedIn,
|
||||
pollOption = null,
|
||||
message = "test",
|
||||
zapType = LnZapEvent.ZapType.PRIVATE,
|
||||
toUserPubHex = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(resultPrivateZap)
|
||||
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
|
||||
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
|
||||
|
||||
if (recepientPK != null && recepientPost != null) {
|
||||
val privateKey =
|
||||
createEncryptionPrivateKey(
|
||||
loggedIn.keyPair.privKey!!.toHexKey(),
|
||||
recepientPost,
|
||||
privateZapRequest.createdAt,
|
||||
)
|
||||
val decodedPrivateZap = PrivateZapRequestBuilder().decryptAnonTag(privateZapRequest.getAnonTag(), privateKey, recepientPK)
|
||||
|
||||
assertNotNull(decodedPrivateZap)
|
||||
} else {
|
||||
fail("Should not be null")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+295
-399
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
@@ -41,104 +42,87 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GiftWrapEventTest {
|
||||
@Test()
|
||||
fun testNip17Utils() {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiver = NostrSignerInternal(KeyPair())
|
||||
val message = "Hola, que tal?"
|
||||
fun testNip17Utils() =
|
||||
runBlocking {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiver = NostrSignerInternal(KeyPair())
|
||||
val message = "Hola, que tal?"
|
||||
|
||||
// Requires 3 tests
|
||||
val countDownLatch = CountDownLatch(3)
|
||||
|
||||
NIP17Factory().createMessageNIP17(
|
||||
ChatMessageEvent.build(
|
||||
message,
|
||||
listOf(PTag(receiver.pubKey, null)),
|
||||
),
|
||||
sender,
|
||||
) { events ->
|
||||
countDownLatch.countDown()
|
||||
val events =
|
||||
NIP17Factory().createMessageNIP17(
|
||||
ChatMessageEvent.build(
|
||||
message,
|
||||
listOf(PTag(receiver.pubKey, null)),
|
||||
),
|
||||
sender,
|
||||
)
|
||||
|
||||
// Simulate Receiver
|
||||
val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) }
|
||||
eventsReceiverGets.forEach {
|
||||
it.unwrap(receiver) { event ->
|
||||
if (event is SealedRumorEvent) {
|
||||
event.unseal(receiver) { innerData ->
|
||||
countDownLatch.countDown()
|
||||
assertEquals(message, innerData.content)
|
||||
}
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
val event = it.unwrapThrowing(receiver)
|
||||
if (event is SealedRumorEvent) {
|
||||
val innerData = event.unsealThrowing(receiver)
|
||||
assertEquals(message, innerData.content)
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate Sender
|
||||
val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) }
|
||||
eventsSenderGets.forEach {
|
||||
it.unwrap(sender) { event ->
|
||||
if (event is SealedRumorEvent) {
|
||||
event.unseal(sender) { innerData ->
|
||||
countDownLatch.countDown()
|
||||
assertEquals(message, innerData.content)
|
||||
}
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
val event = it.unwrapThrowing(sender)
|
||||
if (event is SealedRumorEvent) {
|
||||
val innerData = event.unsealThrowing(sender)
|
||||
assertEquals(message, innerData.content)
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testNip17UtilsForGroups() {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiver1 = NostrSignerInternal(KeyPair())
|
||||
val receiver2 = NostrSignerInternal(KeyPair())
|
||||
val receiver3 = NostrSignerInternal(KeyPair())
|
||||
val receiver4 = NostrSignerInternal(KeyPair())
|
||||
val message = "Hola, que tal?"
|
||||
fun testNip17UtilsForGroups() =
|
||||
runBlocking {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiver1 = NostrSignerInternal(KeyPair())
|
||||
val receiver2 = NostrSignerInternal(KeyPair())
|
||||
val receiver3 = NostrSignerInternal(KeyPair())
|
||||
val receiver4 = NostrSignerInternal(KeyPair())
|
||||
val message = "Hola, que tal?"
|
||||
|
||||
val receivers =
|
||||
listOf(
|
||||
receiver1,
|
||||
receiver2,
|
||||
receiver3,
|
||||
receiver4,
|
||||
)
|
||||
val receivers =
|
||||
listOf(
|
||||
receiver1,
|
||||
receiver2,
|
||||
receiver3,
|
||||
receiver4,
|
||||
)
|
||||
|
||||
val countDownLatch = CountDownLatch(receivers.size + 2)
|
||||
|
||||
NIP17Factory().createMessageNIP17(
|
||||
ChatMessageEvent.build(
|
||||
message,
|
||||
receivers.map { PTag(it.pubKey, null) },
|
||||
),
|
||||
sender,
|
||||
) { events ->
|
||||
countDownLatch.countDown()
|
||||
val events =
|
||||
NIP17Factory().createMessageNIP17(
|
||||
ChatMessageEvent.build(
|
||||
message,
|
||||
receivers.map { PTag(it.pubKey, null) },
|
||||
),
|
||||
sender,
|
||||
)
|
||||
|
||||
// Simulate Receiver
|
||||
receivers.forEach { receiver ->
|
||||
val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) }
|
||||
eventsReceiverGets.forEach {
|
||||
it.unwrap(receiver) { event ->
|
||||
if (event is SealedRumorEvent) {
|
||||
event.unseal(receiver) { innerData ->
|
||||
countDownLatch.countDown()
|
||||
assertEquals(message, innerData.content)
|
||||
}
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
val event = it.unwrapThrowing(receiver)
|
||||
if (event is SealedRumorEvent) {
|
||||
val innerData = event.unsealThrowing(receiver)
|
||||
assertEquals(message, innerData.content)
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,149 +130,126 @@ class GiftWrapEventTest {
|
||||
// Simulate Sender
|
||||
val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) }
|
||||
eventsSenderGets.forEach {
|
||||
it.unwrap(sender) { event ->
|
||||
if (event is SealedRumorEvent) {
|
||||
event.unseal(sender) { innerData ->
|
||||
countDownLatch.countDown()
|
||||
assertEquals(message, innerData.content)
|
||||
}
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
val event = it.unwrapThrowing(sender)
|
||||
if (event is SealedRumorEvent) {
|
||||
val innerData = event.unsealThrowing(sender)
|
||||
assertEquals(message, innerData.content)
|
||||
} else {
|
||||
fail("Wrong Event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testInternalsSimpleMessage() {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiver = NostrSignerInternal(KeyPair())
|
||||
fun testInternalsSimpleMessage() =
|
||||
runBlocking {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiver = NostrSignerInternal(KeyPair())
|
||||
|
||||
val countDownLatch = CountDownLatch(2)
|
||||
|
||||
var giftWrapEventToSender: GiftWrapEvent? = null
|
||||
var giftWrapEventToReceiver: GiftWrapEvent? = null
|
||||
|
||||
sender.sign(
|
||||
ChatMessageEvent.build(
|
||||
msg = "Hi There!",
|
||||
to = listOf(PTag(receiver.pubKey, null)),
|
||||
),
|
||||
) { senderMessage ->
|
||||
val senderMessage =
|
||||
sender.sign(
|
||||
ChatMessageEvent.build(
|
||||
msg = "Hi There!",
|
||||
to = listOf(PTag(receiver.pubKey, null)),
|
||||
),
|
||||
)
|
||||
// MsgFor the Receiver
|
||||
val encMsgFromSenderToReceiver =
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = receiver.pubKey,
|
||||
signer = sender,
|
||||
)
|
||||
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = receiver.pubKey,
|
||||
signer = sender,
|
||||
) { encMsgFromSenderToReceiver ->
|
||||
// Should expose sender
|
||||
assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(encMsgFromSenderToReceiver.tags.isEmpty())
|
||||
// Should expose sender
|
||||
assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(encMsgFromSenderToReceiver.tags.isEmpty())
|
||||
|
||||
val giftWrapToReceiver =
|
||||
GiftWrapEvent.create(
|
||||
event = encMsgFromSenderToReceiver,
|
||||
recipientPubKey = receiver.pubKey,
|
||||
) { giftWrapToReceiver ->
|
||||
// Should not be signed by neither sender nor receiver
|
||||
assertNotEquals(giftWrapToReceiver.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapToReceiver.pubKey, receiver.pubKey)
|
||||
)
|
||||
|
||||
// Should not include sender as recipient
|
||||
assertNotEquals(giftWrapToReceiver.recipientPubKey(), sender.pubKey)
|
||||
// Should not be signed by neither sender nor receiver
|
||||
assertNotEquals(giftWrapToReceiver.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapToReceiver.pubKey, receiver.pubKey)
|
||||
|
||||
// Should be addressed to the receiver
|
||||
assertEquals(giftWrapToReceiver.recipientPubKey(), receiver.pubKey)
|
||||
// Should not include sender as recipient
|
||||
assertNotEquals(giftWrapToReceiver.recipientPubKey(), sender.pubKey)
|
||||
|
||||
giftWrapEventToReceiver = giftWrapToReceiver
|
||||
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
// Should be addressed to the receiver
|
||||
assertEquals(giftWrapToReceiver.recipientPubKey(), receiver.pubKey)
|
||||
|
||||
// MsgFor the Sender
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = sender.pubKey,
|
||||
signer = sender,
|
||||
) { encMsgFromSenderToSender ->
|
||||
// Should expose sender
|
||||
assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(encMsgFromSenderToSender.tags.isEmpty())
|
||||
val encMsgFromSenderToSender =
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = sender.pubKey,
|
||||
signer = sender,
|
||||
)
|
||||
|
||||
// Should expose sender
|
||||
assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(encMsgFromSenderToSender.tags.isEmpty())
|
||||
|
||||
val giftWrapToSender =
|
||||
GiftWrapEvent.create(
|
||||
event = encMsgFromSenderToSender,
|
||||
recipientPubKey = sender.pubKey,
|
||||
) { giftWrapToSender ->
|
||||
// Should not be signed by neither the sender, not the receiver
|
||||
assertNotEquals(giftWrapToSender.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapToSender.pubKey, receiver.pubKey)
|
||||
)
|
||||
|
||||
// Should not be addressed to the receiver
|
||||
assertNotEquals(giftWrapToSender.recipientPubKey(), receiver.pubKey)
|
||||
// Should be addressed to the sender
|
||||
assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey)
|
||||
// Should not be signed by neither the sender, not the receiver
|
||||
assertNotEquals(giftWrapToSender.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapToSender.pubKey, receiver.pubKey)
|
||||
|
||||
giftWrapEventToSender = giftWrapToSender
|
||||
// Should not be addressed to the receiver
|
||||
assertNotEquals(giftWrapToSender.recipientPubKey(), receiver.pubKey)
|
||||
// Should be addressed to the sender
|
||||
assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey)
|
||||
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Done
|
||||
// -----
|
||||
// Start receiving
|
||||
|
||||
// Done
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
// Receiver's side
|
||||
// Makes sure it can only be decrypted by the target user
|
||||
|
||||
// Receiver's side
|
||||
// Makes sure it can only be decrypted by the target user
|
||||
assertNotNull(giftWrapToSender)
|
||||
assertNotNull(giftWrapToReceiver)
|
||||
|
||||
assertNotNull(giftWrapEventToSender)
|
||||
assertNotNull(giftWrapEventToReceiver)
|
||||
|
||||
val countDownDecryptLatch = CountDownLatch(2)
|
||||
|
||||
giftWrapEventToSender!!.unwrap(sender) { unwrappedMsgForSenderBySender ->
|
||||
val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender)
|
||||
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind)
|
||||
assertTrue(unwrappedMsgForSenderBySender is SealedRumorEvent)
|
||||
|
||||
if (unwrappedMsgForSenderBySender is SealedRumorEvent) {
|
||||
unwrappedMsgForSenderBySender.unseal(sender) { unwrappedRumorToSenderBySender ->
|
||||
assertEquals("Hi There!", unwrappedRumorToSenderBySender.content)
|
||||
countDownDecryptLatch.countDown()
|
||||
}
|
||||
val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender)
|
||||
assertEquals("Hi There!", unwrappedRumorToSenderBySender.content)
|
||||
|
||||
unwrappedMsgForSenderBySender.unseal(receiver) { _ ->
|
||||
fail(
|
||||
"Should not be able to decrypt msg for the sender by the sender but decrypted with receiver",
|
||||
)
|
||||
unwrappedMsgForSenderBySender.unsealOrNull(receiver)?.let { _ ->
|
||||
fail("Should not be able to decrypt msg for the sender by the sender but decrypted with receiver")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
giftWrapEventToReceiver!!.unwrap(sender) { _ ->
|
||||
fail("Should not be able to decrypt msg for the receiver decrypted by the sender")
|
||||
}
|
||||
giftWrapToReceiver.unwrapOrNull(sender)?.let { _ ->
|
||||
fail("Should not be able to decrypt msg for the receiver decrypted by the sender")
|
||||
}
|
||||
|
||||
giftWrapEventToSender!!.unwrap(receiver) { _ ->
|
||||
fail("Should not be able to decrypt msg for the sender decrypted by the receiver")
|
||||
}
|
||||
giftWrapToReceiver.unwrapOrNull(receiver)?.let { _ ->
|
||||
fail("Should not be able to decrypt msg for the sender decrypted by the receiver")
|
||||
}
|
||||
|
||||
giftWrapEventToReceiver!!.unwrap(receiver) { unwrappedMsgForReceiverByReceiver ->
|
||||
val unwrappedMsgForReceiverByReceiver = giftWrapToReceiver.unwrapThrowing(receiver)
|
||||
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverByReceiver.kind)
|
||||
assertTrue(unwrappedMsgForReceiverByReceiver is SealedRumorEvent)
|
||||
|
||||
if (unwrappedMsgForReceiverByReceiver is SealedRumorEvent) {
|
||||
unwrappedMsgForReceiverByReceiver.unseal(receiver) { unwrappedRumorToReceiverByReceiver ->
|
||||
assertEquals("Hi There!", unwrappedRumorToReceiverByReceiver?.content)
|
||||
countDownDecryptLatch.countDown()
|
||||
}
|
||||
val unwrappedRumorToReceiverByReceiver = unwrappedMsgForReceiverByReceiver.unsealThrowing(receiver)
|
||||
assertEquals("Hi There!", unwrappedRumorToReceiverByReceiver.content)
|
||||
|
||||
unwrappedMsgForReceiverByReceiver.unseal(sender) { unwrappedRumorToReceiverBySender ->
|
||||
unwrappedMsgForReceiverByReceiver.unsealOrNull(sender)?.let { _ ->
|
||||
fail(
|
||||
"Should not be able to decrypt msg for the receiver by the receiver but decrypted with the sender",
|
||||
)
|
||||
@@ -296,227 +257,194 @@ class GiftWrapEventTest {
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(countDownDecryptLatch.await(1, TimeUnit.SECONDS))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testInternalsGroupMessage() {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiverA = NostrSignerInternal(KeyPair())
|
||||
val receiverB = NostrSignerInternal(KeyPair())
|
||||
fun testInternalsGroupMessage() =
|
||||
runBlocking {
|
||||
val sender = NostrSignerInternal(KeyPair())
|
||||
val receiverA = NostrSignerInternal(KeyPair())
|
||||
val receiverB = NostrSignerInternal(KeyPair())
|
||||
|
||||
val countDownLatch = CountDownLatch(3)
|
||||
val senderMessage =
|
||||
sender.sign(
|
||||
ChatMessageEvent.build(
|
||||
msg = "Who is going to the party tonight?",
|
||||
to = listOf(PTag(receiverA.pubKey), PTag(receiverB.pubKey)),
|
||||
),
|
||||
)
|
||||
|
||||
var giftWrapEventToSender: GiftWrapEvent? = null
|
||||
var giftWrapEventToReceiverA: GiftWrapEvent? = null
|
||||
var giftWrapEventToReceiverB: GiftWrapEvent? = null
|
||||
val msgFromSenderToReceiverA =
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = receiverA.pubKey,
|
||||
signer = sender,
|
||||
)
|
||||
|
||||
sender.sign(
|
||||
ChatMessageEvent.build(
|
||||
msg = "Who is going to the party tonight?",
|
||||
to = listOf(PTag(receiverA.pubKey), PTag(receiverB.pubKey)),
|
||||
),
|
||||
) { senderMessage ->
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = receiverA.pubKey,
|
||||
signer = sender,
|
||||
) { msgFromSenderToReceiverA ->
|
||||
// Should expose sender
|
||||
assertEquals(msgFromSenderToReceiverA.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(msgFromSenderToReceiverA.tags.isEmpty())
|
||||
// Should expose sender
|
||||
assertEquals(msgFromSenderToReceiverA.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(msgFromSenderToReceiverA.tags.isEmpty())
|
||||
|
||||
val giftWrapForReceiverA =
|
||||
GiftWrapEvent.create(
|
||||
event = msgFromSenderToReceiverA,
|
||||
recipientPubKey = receiverA.pubKey,
|
||||
) { giftWrapForReceiverA ->
|
||||
// Should not be signed by neither sender nor receiver
|
||||
assertNotEquals(giftWrapForReceiverA.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverA.pubKey, receiverA.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverA.pubKey, receiverB.pubKey)
|
||||
)
|
||||
|
||||
// Should not include sender as recipient
|
||||
assertNotEquals(giftWrapForReceiverA.recipientPubKey(), sender.pubKey)
|
||||
// Should not be signed by neither sender nor receiver
|
||||
assertNotEquals(giftWrapForReceiverA.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverA.pubKey, receiverA.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverA.pubKey, receiverB.pubKey)
|
||||
|
||||
// Should be addressed to the receiver
|
||||
assertEquals(giftWrapForReceiverA.recipientPubKey(), receiverA.pubKey)
|
||||
// Should not include sender as recipient
|
||||
assertNotEquals(giftWrapForReceiverA.recipientPubKey(), sender.pubKey)
|
||||
|
||||
giftWrapEventToReceiverA = giftWrapForReceiverA
|
||||
// Should be addressed to the receiver
|
||||
assertEquals(giftWrapForReceiverA.recipientPubKey(), receiverA.pubKey)
|
||||
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
val msgFromSenderToReceiverB =
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = receiverB.pubKey,
|
||||
signer = sender,
|
||||
)
|
||||
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = receiverB.pubKey,
|
||||
signer = sender,
|
||||
) { msgFromSenderToReceiverB ->
|
||||
// Should expose sender
|
||||
assertEquals(msgFromSenderToReceiverB.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(msgFromSenderToReceiverB.tags.isEmpty())
|
||||
// Should expose sender
|
||||
assertEquals(msgFromSenderToReceiverB.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(msgFromSenderToReceiverB.tags.isEmpty())
|
||||
|
||||
val giftWrapForReceiverB =
|
||||
GiftWrapEvent.create(
|
||||
event = msgFromSenderToReceiverB,
|
||||
recipientPubKey = receiverB.pubKey,
|
||||
) { giftWrapForReceiverB ->
|
||||
// Should not be signed by neither sender nor receiver
|
||||
assertNotEquals(giftWrapForReceiverB.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverB.pubKey, receiverA.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverB.pubKey, receiverB.pubKey)
|
||||
)
|
||||
|
||||
// Should not include sender as recipient
|
||||
assertNotEquals(giftWrapForReceiverB.recipientPubKey(), sender.pubKey)
|
||||
// Should not be signed by neither sender nor receiver
|
||||
assertNotEquals(giftWrapForReceiverB.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverB.pubKey, receiverA.pubKey)
|
||||
assertNotEquals(giftWrapForReceiverB.pubKey, receiverB.pubKey)
|
||||
|
||||
// Should be addressed to the receiver
|
||||
assertEquals(giftWrapForReceiverB.recipientPubKey(), receiverB.pubKey)
|
||||
// Should not include sender as recipient
|
||||
assertNotEquals(giftWrapForReceiverB.recipientPubKey(), sender.pubKey)
|
||||
|
||||
giftWrapEventToReceiverB = giftWrapForReceiverB
|
||||
// Should be addressed to the receiver
|
||||
assertEquals(giftWrapForReceiverB.recipientPubKey(), receiverB.pubKey)
|
||||
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
val msgFromSenderToSender =
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = sender.pubKey,
|
||||
signer = sender,
|
||||
)
|
||||
|
||||
SealedRumorEvent.create(
|
||||
event = senderMessage,
|
||||
encryptTo = sender.pubKey,
|
||||
signer = sender,
|
||||
) { msgFromSenderToSender ->
|
||||
// Should expose sender
|
||||
assertEquals(msgFromSenderToSender.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(msgFromSenderToSender.tags.isEmpty())
|
||||
// Should expose sender
|
||||
assertEquals(msgFromSenderToSender.pubKey, sender.pubKey)
|
||||
// Should not expose receiver
|
||||
assertTrue(msgFromSenderToSender.tags.isEmpty())
|
||||
|
||||
val giftWrapToSender =
|
||||
GiftWrapEvent.create(
|
||||
event = msgFromSenderToSender,
|
||||
recipientPubKey = sender.pubKey,
|
||||
) { giftWrapToSender ->
|
||||
// Should not be signed by neither the sender, not the receiver
|
||||
assertNotEquals(giftWrapToSender.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapToSender.pubKey, receiverA.pubKey)
|
||||
assertNotEquals(giftWrapToSender.pubKey, receiverB.pubKey)
|
||||
)
|
||||
|
||||
// Should not be addressed to the receiver
|
||||
assertNotEquals(giftWrapToSender.recipientPubKey(), receiverA.pubKey)
|
||||
assertNotEquals(giftWrapToSender.recipientPubKey(), receiverB.pubKey)
|
||||
// Should be addressed to the sender
|
||||
assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey)
|
||||
// Should not be signed by neither the sender, not the receiver
|
||||
assertNotEquals(giftWrapToSender.pubKey, sender.pubKey)
|
||||
assertNotEquals(giftWrapToSender.pubKey, receiverA.pubKey)
|
||||
assertNotEquals(giftWrapToSender.pubKey, receiverB.pubKey)
|
||||
|
||||
giftWrapEventToSender = giftWrapToSender
|
||||
// Should not be addressed to the receiver
|
||||
assertNotEquals(giftWrapToSender.recipientPubKey(), receiverA.pubKey)
|
||||
assertNotEquals(giftWrapToSender.recipientPubKey(), receiverB.pubKey)
|
||||
// Should be addressed to the sender
|
||||
assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey)
|
||||
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Done
|
||||
// -----
|
||||
// Decrypting
|
||||
|
||||
// Done
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
// Receiver's side
|
||||
// Makes sure it can only be decrypted by the target user
|
||||
|
||||
// Receiver's side
|
||||
// Makes sure it can only be decrypted by the target user
|
||||
assertNotNull(giftWrapToSender)
|
||||
assertNotNull(giftWrapForReceiverA)
|
||||
assertNotNull(giftWrapForReceiverB)
|
||||
|
||||
assertNotNull(giftWrapEventToSender)
|
||||
assertNotNull(giftWrapEventToReceiverA)
|
||||
assertNotNull(giftWrapEventToReceiverB)
|
||||
|
||||
val countDownDecryptLatch = CountDownLatch(3)
|
||||
|
||||
giftWrapEventToSender?.unwrap(sender) { unwrappedMsgForSenderBySender ->
|
||||
val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender)
|
||||
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind)
|
||||
|
||||
if (unwrappedMsgForSenderBySender is SealedRumorEvent) {
|
||||
unwrappedMsgForSenderBySender.unseal(receiverA) { unwrappedRumorToSenderByReceiverA ->
|
||||
unwrappedMsgForSenderBySender.unsealOrNull(receiverA)?.let { _ ->
|
||||
fail()
|
||||
}
|
||||
|
||||
unwrappedMsgForSenderBySender.unseal(receiverB) { unwrappedRumorToSenderByReceiverB ->
|
||||
unwrappedMsgForSenderBySender.unsealOrNull(receiverB)?.let { _ ->
|
||||
fail()
|
||||
}
|
||||
|
||||
unwrappedMsgForSenderBySender.unseal(sender) { unwrappedRumorToSenderBySender ->
|
||||
assertEquals(
|
||||
"Who is going to the party tonight?",
|
||||
unwrappedRumorToSenderBySender.content,
|
||||
)
|
||||
}
|
||||
val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender)
|
||||
assertEquals("Who is going to the party tonight?", unwrappedRumorToSenderBySender.content)
|
||||
}
|
||||
|
||||
countDownDecryptLatch.countDown()
|
||||
}
|
||||
giftWrapForReceiverA.unwrapOrNull(sender)?.let { unwrappedMsgForReceiverBySenderA ->
|
||||
fail("Should not be able to decode msg to the receiver A with the sender's key")
|
||||
}
|
||||
|
||||
giftWrapEventToReceiverA!!.unwrap(sender) { unwrappedMsgForReceiverBySenderA ->
|
||||
fail("Should not be able to decode msg to the receiver A with the sender's key")
|
||||
}
|
||||
giftWrapForReceiverB.unwrapOrNull(sender)?.let { unwrappedMsgForReceiverBySenderB ->
|
||||
fail("Should not be able to decode msg to the receiver B with the sender's key")
|
||||
}
|
||||
|
||||
giftWrapEventToReceiverB!!.unwrap(sender) { unwrappedMsgForReceiverBySenderB ->
|
||||
fail("Should not be able to decode msg to the receiver B with the sender's key")
|
||||
}
|
||||
giftWrapToSender.unwrapOrNull(receiverA)?.let {
|
||||
fail("Should not be able to decode msg to sender with the receiver A's key")
|
||||
}
|
||||
|
||||
giftWrapEventToSender!!.unwrap(receiverA) {
|
||||
fail("Should not be able to decode msg to sender with the receiver A's key")
|
||||
}
|
||||
|
||||
giftWrapEventToReceiverA!!.unwrap(receiverA) { unwrappedMsgForReceiverAByReceiverA ->
|
||||
val unwrappedMsgForReceiverAByReceiverA = giftWrapForReceiverA.unwrapThrowing(receiverA)
|
||||
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverAByReceiverA.kind)
|
||||
|
||||
if (unwrappedMsgForReceiverAByReceiverA is SealedRumorEvent) {
|
||||
unwrappedMsgForReceiverAByReceiverA.unseal(receiverA) { unwrappedRumorToReceiverAByReceiverA ->
|
||||
assertEquals(
|
||||
"Who is going to the party tonight?",
|
||||
unwrappedRumorToReceiverAByReceiverA.content,
|
||||
)
|
||||
}
|
||||
val unwrappedRumorToReceiverAByReceiverA = unwrappedMsgForReceiverAByReceiverA.unsealThrowing(receiverA)
|
||||
assertEquals("Who is going to the party tonight?", unwrappedRumorToReceiverAByReceiverA.content)
|
||||
|
||||
unwrappedMsgForReceiverAByReceiverA.unseal(sender) { unwrappedRumorToReceiverABySender ->
|
||||
unwrappedMsgForReceiverAByReceiverA.unsealOrNull(sender)?.let { unwrappedRumorToReceiverABySender ->
|
||||
fail()
|
||||
}
|
||||
|
||||
unwrappedMsgForReceiverAByReceiverA.unseal(receiverB) { unwrappedRumorToReceiverAByReceiverB ->
|
||||
unwrappedMsgForReceiverAByReceiverA.unsealOrNull(receiverB)?.let { unwrappedRumorToReceiverAByReceiverB ->
|
||||
fail()
|
||||
}
|
||||
}
|
||||
|
||||
countDownDecryptLatch.countDown()
|
||||
}
|
||||
giftWrapForReceiverB.unwrapOrNull(receiverA)?.let {
|
||||
fail("Should not be able to decode msg to sender with the receiver A's key")
|
||||
}
|
||||
|
||||
giftWrapEventToReceiverB!!.unwrap(receiverA) {
|
||||
fail("Should not be able to decode msg to sender with the receiver A's key")
|
||||
}
|
||||
giftWrapToSender.unwrapOrNull(receiverB)?.let { unwrappedMsgForSenderByReceiverB ->
|
||||
fail("Should not be able to decode msg to sender with the receiver B's key")
|
||||
}
|
||||
giftWrapForReceiverA.unwrapOrNull(receiverB)?.let { unwrappedMsgForReceiverAByReceiverB ->
|
||||
fail("Should not be able to decode msg to receiver A with the receiver B's key")
|
||||
}
|
||||
|
||||
giftWrapEventToSender!!.unwrap(receiverB) { unwrappedMsgForSenderByReceiverB ->
|
||||
fail("Should not be able to decode msg to sender with the receiver B's key")
|
||||
}
|
||||
giftWrapEventToReceiverA!!.unwrap(receiverB) { unwrappedMsgForReceiverAByReceiverB ->
|
||||
fail("Should not be able to decode msg to receiver A with the receiver B's key")
|
||||
}
|
||||
giftWrapEventToReceiverB!!.unwrap(receiverB) { unwrappedMsgForReceiverBByReceiverB ->
|
||||
val unwrappedMsgForReceiverBByReceiverB = giftWrapForReceiverB.unwrapThrowing(receiverB)
|
||||
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverBByReceiverB.kind)
|
||||
|
||||
if (unwrappedMsgForReceiverBByReceiverB is SealedRumorEvent) {
|
||||
unwrappedMsgForReceiverBByReceiverB.unseal(receiverA) { unwrappedRumorToReceiverBByReceiverA ->
|
||||
unwrappedMsgForReceiverBByReceiverB.unsealOrNull(receiverA)?.let { unwrappedRumorToReceiverBByReceiverA ->
|
||||
fail()
|
||||
}
|
||||
|
||||
unwrappedMsgForReceiverBByReceiverB.unseal(receiverB) { unwrappedRumorToReceiverBByReceiverB ->
|
||||
assertEquals(
|
||||
"Who is going to the party tonight?",
|
||||
unwrappedRumorToReceiverBByReceiverB.content,
|
||||
)
|
||||
val unwrappedRumorToReceiverBByReceiverB = unwrappedMsgForReceiverBByReceiverB.unsealThrowing(receiverB)
|
||||
assertEquals(
|
||||
"Who is going to the party tonight?",
|
||||
unwrappedRumorToReceiverBByReceiverB.content,
|
||||
)
|
||||
|
||||
countDownDecryptLatch.countDown()
|
||||
}
|
||||
|
||||
unwrappedMsgForReceiverBByReceiverB.unseal(sender) { unwrappedRumorToReceiverBBySender ->
|
||||
unwrappedMsgForReceiverBByReceiverB.unsealOrNull(sender)?.let { unwrappedRumorToReceiverBBySender ->
|
||||
fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(countDownDecryptLatch.await(1, TimeUnit.SECONDS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCaseFromAmethyst1() {
|
||||
val json =
|
||||
@@ -537,18 +465,14 @@ class GiftWrapEventTest {
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
var rumor: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
val privateKey = "de6152a85a0dea3b09a08a6f8139a314d498a7b52f7e5c28858b64270abd4c70"
|
||||
unwrapUnsealRumor(json, privateKey) {
|
||||
rumor = it
|
||||
onDone()
|
||||
val rumor: Event =
|
||||
runBlocking {
|
||||
val privateKey = "de6152a85a0dea3b09a08a6f8139a314d498a7b52f7e5c28858b64270abd4c70"
|
||||
unwrapUnsealRumor(json, privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(rumor)
|
||||
assertEquals("Hola, que tal?", rumor?.content)
|
||||
assertEquals("Hola, que tal?", rumor.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -573,17 +497,13 @@ class GiftWrapEventTest {
|
||||
|
||||
val privateKey = "409ff7654141eaa16cd2161fe5bd127aeaef71f270c67587474b78998a8e3533"
|
||||
|
||||
var rumor: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
unwrapUnsealRumor(json, privateKey) {
|
||||
rumor = it
|
||||
onDone()
|
||||
val rumor: Event =
|
||||
runBlocking {
|
||||
unwrapUnsealRumor(json, privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(rumor)
|
||||
assertEquals("Hola, que tal?", rumor?.content)
|
||||
assertEquals("Hola, que tal?", rumor.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -610,17 +530,14 @@ class GiftWrapEventTest {
|
||||
""".trimIndent()
|
||||
|
||||
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
|
||||
var rumor: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
unwrapUnsealRumor(json, privateKey) {
|
||||
rumor = it
|
||||
onDone()
|
||||
val rumor: Event =
|
||||
runBlocking {
|
||||
unwrapUnsealRumor(json, privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(rumor)
|
||||
assertEquals("test", rumor?.content)
|
||||
assertEquals("test", rumor.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -645,21 +562,17 @@ class GiftWrapEventTest {
|
||||
|
||||
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
|
||||
|
||||
var rumor: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
unwrapUnsealRumor(json, privateKey) {
|
||||
rumor = it
|
||||
onDone()
|
||||
val rumor =
|
||||
runBlocking {
|
||||
unwrapUnsealRumor(json, privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("asdfasdfasdf", rumor?.content)
|
||||
assertEquals(1690659269L, rumor?.createdAt)
|
||||
assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", rumor?.id)
|
||||
assertEquals(14, rumor?.kind)
|
||||
assertEquals("subject", rumor?.tags?.firstOrNull()?.get(0))
|
||||
assertEquals("test", rumor?.tags?.firstOrNull()?.get(1))
|
||||
assertEquals("asdfasdfasdf", rumor.content)
|
||||
assertEquals(1690659269L, rumor.createdAt)
|
||||
assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", rumor.id)
|
||||
assertEquals(14, rumor.kind)
|
||||
assertEquals("subject", rumor.tags.firstOrNull()?.get(0))
|
||||
assertEquals("test", rumor.tags.firstOrNull()?.get(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -684,33 +597,28 @@ class GiftWrapEventTest {
|
||||
|
||||
val privateKey = "7dd22cafc512c0bc363a259f6dcda515b13ae3351066d7976fd0bb79cbd0d700"
|
||||
|
||||
var rumor: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
unwrapUnsealRumor(json, privateKey) {
|
||||
rumor = it
|
||||
onDone()
|
||||
val rumor =
|
||||
runBlocking {
|
||||
unwrapUnsealRumor(json, privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", rumor?.id)
|
||||
assertEquals("Howdy", rumor?.content)
|
||||
assertEquals(1690833960L, rumor?.createdAt)
|
||||
assertEquals(14, rumor?.kind)
|
||||
assertEquals("p", rumor?.tags?.firstOrNull()?.get(0))
|
||||
assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", rumor.id)
|
||||
assertEquals("Howdy", rumor.content)
|
||||
assertEquals(1690833960L, rumor.createdAt)
|
||||
assertEquals(14, rumor.kind)
|
||||
assertEquals("p", rumor.tags.firstOrNull()?.get(0))
|
||||
assertEquals(
|
||||
"b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78",
|
||||
rumor?.tags?.firstOrNull()?.get(1),
|
||||
rumor.tags.firstOrNull()?.get(1),
|
||||
)
|
||||
assertEquals("subject", rumor?.tags?.getOrNull(1)?.get(0))
|
||||
assertEquals("Stuff", rumor?.tags?.getOrNull(1)?.get(1))
|
||||
assertEquals("subject", rumor.tags.getOrNull(1)?.get(0))
|
||||
assertEquals("Stuff", rumor.tags.getOrNull(1)?.get(1))
|
||||
}
|
||||
|
||||
fun unwrapUnsealRumor(
|
||||
suspend fun unwrapUnsealRumor(
|
||||
json: String,
|
||||
privateKey: HexKey,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
): Event {
|
||||
val pkBytes = NostrSignerInternal(KeyPair(privateKey.hexToByteArray()))
|
||||
|
||||
val wrap = Event.fromJson(json) as GiftWrapEvent
|
||||
@@ -718,13 +626,13 @@ class GiftWrapEventTest {
|
||||
|
||||
assertEquals(pkBytes.pubKey, wrap.recipientPubKey())
|
||||
|
||||
wrap.unwrap(pkBytes) { event ->
|
||||
if (event is SealedRumorEvent) {
|
||||
event.unseal(pkBytes, onReady)
|
||||
} else {
|
||||
println(event.toJson())
|
||||
fail("Event is not a Sealed Rumor")
|
||||
}
|
||||
val event = wrap.unwrapThrowing(pkBytes)
|
||||
return if (event is SealedRumorEvent) {
|
||||
event.unsealThrowing(pkBytes)
|
||||
} else {
|
||||
println(event.toJson())
|
||||
fail("Event is not a Sealed Rumor")
|
||||
throw Exception("Event is not a Sealed Rumor")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,23 +658,11 @@ class GiftWrapEventTest {
|
||||
val wrap = Event.fromJson(msg) as GiftWrapEvent
|
||||
wrap.checkSignature()
|
||||
|
||||
var event: Event? = null
|
||||
|
||||
wait1SecondForResult { onDone ->
|
||||
wrap.unwrap(receiversPrivateKey) {
|
||||
event = it
|
||||
onDone()
|
||||
val event =
|
||||
runBlocking {
|
||||
wrap.unwrapThrowing(receiversPrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(event)
|
||||
}
|
||||
}
|
||||
|
||||
fun wait1SecondForResult(run: (onDone: () -> Unit) -> Unit) {
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
|
||||
run { countDownLatch.countDown() }
|
||||
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
@@ -68,22 +67,23 @@ 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.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
|
||||
@@ -124,19 +124,30 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
|
||||
interface EventBuilder {
|
||||
fun build(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
): Event
|
||||
}
|
||||
|
||||
class EventFactory {
|
||||
companion object {
|
||||
val factories: MutableMap<Int, (HexKey, HexKey, Long, Array<Array<String>>, String, HexKey) -> Event> = mutableMapOf()
|
||||
val factories: MutableMap<Int, EventBuilder> = mutableMapOf()
|
||||
|
||||
fun create(
|
||||
id: String,
|
||||
pubKey: String,
|
||||
fun <T : Event> create(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: String,
|
||||
): Event =
|
||||
sig: HexKey,
|
||||
): T =
|
||||
when (kind) {
|
||||
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -265,12 +276,9 @@ class EventFactory {
|
||||
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
else -> {
|
||||
factories[kind]?.let {
|
||||
return it(id, pubKey, createdAt, tags, content, sig)
|
||||
}
|
||||
|
||||
Event(id, pubKey, createdAt, kind, tags, content, sig)
|
||||
factories[kind]?.build(id, pubKey, createdAt, tags, content, sig)
|
||||
?: Event(id, pubKey, createdAt, kind, tags, content, sig)
|
||||
}
|
||||
}
|
||||
} as T
|
||||
}
|
||||
}
|
||||
|
||||
+26
-32
@@ -45,68 +45,62 @@ class DecoupledCipher {
|
||||
pubKey = fromPublicKey.hexToByteArray(),
|
||||
)
|
||||
|
||||
fun encrypt(
|
||||
suspend fun encrypt(
|
||||
decryptedContent: String,
|
||||
toPublicKey: HexKey,
|
||||
fromKeyList: EncryptionKeyListEvent,
|
||||
toKeyList: EncryptionKeyListEvent,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
): String? {
|
||||
val toKeys = toKeyList.keys()
|
||||
val sendToKey = if (toKeys.isEmpty()) toKeyList.pubKey else toKeys.random().pubkey
|
||||
|
||||
val fromKeys = fromKeyList.keys()
|
||||
|
||||
// uses the main key
|
||||
if (fromKeys.isEmpty()) {
|
||||
signer.nip44Encrypt(decryptedContent, sendToKey, onReady)
|
||||
return if (fromKeys.isEmpty()) {
|
||||
signer.nip44Encrypt(decryptedContent, sendToKey)
|
||||
} else {
|
||||
val keyToUse = fromKeys.random()
|
||||
|
||||
EncryptionKeyCache.getOrLoad(
|
||||
deriveFromPubKey = signer.pubKey,
|
||||
nonce = keyToUse.nonce,
|
||||
load = { onLoaded ->
|
||||
signer.deriveKey(keyToUse.nonce) { newPrivKey ->
|
||||
onLoaded(newPrivKey.hexToByteArray())
|
||||
}
|
||||
},
|
||||
) { derivedPrivKey ->
|
||||
onReady(innerEncrypt(decryptedContent, derivedPrivKey, sendToKey))
|
||||
}
|
||||
EncryptionKeyCache
|
||||
.getOrLoad(
|
||||
deriveFromPubKey = signer.pubKey,
|
||||
nonce = keyToUse.nonce,
|
||||
load = { signer.deriveKey(keyToUse.nonce).hexToByteArray() },
|
||||
)?.let { derivedPrivKey ->
|
||||
return innerEncrypt(decryptedContent, derivedPrivKey, sendToKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
suspend fun decrypt(
|
||||
encryptedContent: String,
|
||||
fromPublicKey: HexKey,
|
||||
toPublicKey: HexKey,
|
||||
fromKeyList: EncryptionKeyListEvent,
|
||||
toEncryptedKeyList: EncryptionKeyListEvent,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
): String? {
|
||||
val fromKeys = fromKeyList.keys()
|
||||
val sentFromKey = if (fromKeys.isEmpty()) fromKeyList.pubKey else fromKeys.random().pubkey
|
||||
|
||||
val keyToUse = toEncryptedKeyList.keys().firstOrNull { it.pubkey == toPublicKey }
|
||||
|
||||
// uses the main key
|
||||
if (signer.pubKey == toPublicKey) {
|
||||
signer.nip44Decrypt(encryptedContent, sentFromKey, onReady)
|
||||
return if (signer.pubKey == toPublicKey) {
|
||||
signer.nip44Decrypt(encryptedContent, sentFromKey)
|
||||
} else if (keyToUse != null) {
|
||||
EncryptionKeyCache.getOrLoad(
|
||||
deriveFromPubKey = signer.pubKey,
|
||||
nonce = keyToUse.nonce,
|
||||
load = { onLoaded ->
|
||||
signer.deriveKey(keyToUse.nonce) { newPrivKey ->
|
||||
onLoaded(newPrivKey.hexToByteArray())
|
||||
}
|
||||
},
|
||||
) { derivedPrivKey ->
|
||||
innerDecrypt(encryptedContent, derivedPrivKey, sentFromKey)?.let { onReady(it) }
|
||||
}
|
||||
EncryptionKeyCache
|
||||
.getOrLoad(
|
||||
deriveFromPubKey = signer.pubKey,
|
||||
nonce = keyToUse.nonce,
|
||||
load = { signer.deriveKey(keyToUse.nonce).hexToByteArray() },
|
||||
)?.let { derivedPrivKey ->
|
||||
innerDecrypt(encryptedContent, derivedPrivKey, sentFromKey)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -35,29 +35,29 @@ object EncryptionKeyCache {
|
||||
deriveFromPubKey: HexKey,
|
||||
nonce: HexKey,
|
||||
privKey: ByteArray,
|
||||
) = sharedNonceKeyCache.put(idx(deriveFromPubKey, nonce), privKey)
|
||||
): ByteArray? = sharedNonceKeyCache.put(idx(deriveFromPubKey, nonce), privKey)
|
||||
|
||||
fun get(
|
||||
deriveFromPubKey: HexKey,
|
||||
nonce: HexKey,
|
||||
) = sharedNonceKeyCache.get(idx(deriveFromPubKey, nonce))
|
||||
): ByteArray? = sharedNonceKeyCache.get(idx(deriveFromPubKey, nonce))
|
||||
|
||||
inline fun getOrLoad(
|
||||
deriveFromPubKey: HexKey,
|
||||
nonce: HexKey,
|
||||
load: (onLoaded: (privKey: ByteArray) -> Unit) -> Unit,
|
||||
crossinline whenReady: (privKey: ByteArray) -> Unit,
|
||||
) {
|
||||
load: () -> ByteArray?,
|
||||
): ByteArray? {
|
||||
val cachedPrivKey = get(deriveFromPubKey, nonce)
|
||||
if (cachedPrivKey != null) {
|
||||
whenReady(cachedPrivKey)
|
||||
return
|
||||
return cachedPrivKey
|
||||
}
|
||||
|
||||
load { newPrivKey ->
|
||||
val newPrivKey = load()
|
||||
newPrivKey?.let {
|
||||
put(deriveFromPubKey, nonce, newPrivKey)
|
||||
whenReady(newPrivKey)
|
||||
newPrivKey
|
||||
}
|
||||
return newPrivKey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-173
@@ -1,173 +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.experimental.edits
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.experimental.edits.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class PrivateOutboxRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
fun relays(): List<NormalizedRelayUrl>? =
|
||||
tags
|
||||
.mapNotNull(RelayTag::parse)
|
||||
.plus(
|
||||
privateTagsCache?.mapNotNull(RelayTag::parse) ?: emptyList(),
|
||||
).ifEmpty { null }
|
||||
|
||||
fun cachedPrivateTags(): Array<Array<String>>? = privateTagsCache
|
||||
|
||||
fun privateTags(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) {
|
||||
onReady(emptyArray())
|
||||
return
|
||||
}
|
||||
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
signer.nip44Decrypt(content, pubKey) {
|
||||
try {
|
||||
privateTagsCache = JsonMapper.mapper.readValue<TagArray>(it)
|
||||
privateTagsCache?.let { onReady(it) }
|
||||
} catch (e: Throwable) {
|
||||
Log.w("PrivateOutboxRelayListEvent", "Error parsing the JSON: ${e.message}. Json `$it` from event `${toNostrUri()}`")
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("PrivateOutboxRelayListEvent", "Error decrypting content: ${e.message}. Event: `${toNostrUri()}`")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10013
|
||||
val TAGS = arrayOf(AltTag.assemble("Relay list to store private content from this author"))
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun encryptTags(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val msg = JsonMapper.mapper.writeValueAsString(privateTags)
|
||||
|
||||
signer.nip44Encrypt(
|
||||
msg,
|
||||
signer.pubKey,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: PrivateOutboxRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.privateTagsCache
|
||||
?.filter(RelayTag::notMatch)
|
||||
?.plus(
|
||||
relays.map {
|
||||
RelayTag.assemble(it)
|
||||
},
|
||||
)?.toTypedArray() ?: emptyArray()
|
||||
|
||||
encryptTags(tags, signer) {
|
||||
signer.sign<PrivateOutboxRelayListEvent>(createdAt, KIND, TAGS, it) {
|
||||
it.privateTagsCache = tags
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
val privateTagArray = createTagArray(relays)
|
||||
encryptTags(privateTagArray, signer) { privateTags ->
|
||||
signer.sign<PrivateOutboxRelayListEvent>(createdAt, KIND, TAGS, privateTags) {
|
||||
it.privateTagsCache = privateTagArray
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -45,15 +45,14 @@ class TextNoteModificationEvent(
|
||||
const val KIND = 1010
|
||||
const val ALT = "Content Change Event"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
content: String,
|
||||
eventId: HexKey,
|
||||
notify: HexKey?,
|
||||
summary: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TextNoteModificationEvent) -> Unit,
|
||||
) {
|
||||
): TextNoteModificationEvent {
|
||||
val tags = mutableListOf(arrayOf("e", eventId))
|
||||
|
||||
notify?.let {
|
||||
@@ -66,7 +65,7 @@ class TextNoteModificationEvent(
|
||||
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.ephemChat.chat
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
|
||||
@@ -39,4 +40,6 @@ data class RoomId(
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fun toTagArray() = RoomIdTag.assemble(id, relayUrl)
|
||||
}
|
||||
|
||||
+92
-66
@@ -22,15 +22,22 @@ package com.vitorpamplona.quartz.experimental.ephemChat.list
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms
|
||||
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.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
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
|
||||
import java.lang.reflect.Modifier.isPrivate
|
||||
|
||||
@Immutable
|
||||
class EphemeralChatListEvent(
|
||||
@@ -41,31 +48,6 @@ class EphemeralChatListEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var publicAndPrivateEventCache: Set<RoomId>? = null
|
||||
|
||||
fun publicAndPrivateRoomIds(
|
||||
signer: NostrSigner,
|
||||
onReady: (Set<RoomId>) -> Unit,
|
||||
) {
|
||||
publicAndPrivateEventCache?.let { eventList ->
|
||||
onReady(eventList)
|
||||
return
|
||||
}
|
||||
|
||||
privateTags(signer) {
|
||||
val set = filterRooms(it)
|
||||
publicAndPrivateEventCache = set
|
||||
onReady(set)
|
||||
}
|
||||
}
|
||||
|
||||
fun filterRooms(privateTags: Array<Array<String>>): Set<RoomId> {
|
||||
val privateRooms = privateTags.mapNotNull(RoomIdTag::parse)
|
||||
val publicRooms = tags.mapNotNull(RoomIdTag::parse)
|
||||
|
||||
return (privateRooms + publicRooms).toSet()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10023
|
||||
const val ALT = "Ephemeral Chat List"
|
||||
@@ -73,76 +55,120 @@ class EphemeralChatListEvent(
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createRoom(
|
||||
suspend fun create(
|
||||
room: RoomId,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
val tags = arrayOf(RoomIdTag.assemble(room))
|
||||
): EphemeralChatListEvent =
|
||||
if (isPrivate) {
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = tags,
|
||||
create(
|
||||
publicRooms = emptyList(),
|
||||
privateRooms = listOf(room),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(encryptedTags, emptyArray(), signer, createdAt, onReady)
|
||||
}
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create("", tags, signer, createdAt, onReady)
|
||||
create(
|
||||
publicRooms = listOf(room),
|
||||
privateRooms = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoom(
|
||||
suspend fun add(
|
||||
earlierVersion: EphemeralChatListEvent,
|
||||
room: RoomId,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.removeAll(
|
||||
earlierVersion,
|
||||
RoomIdTag.assemble(room.id, room.relayUrl),
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
): EphemeralChatListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
tags = earlierVersion.tags,
|
||||
privateTags = privateTags.plus(room.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(room.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoom(
|
||||
suspend fun remove(
|
||||
earlierVersion: EphemeralChatListEvent,
|
||||
room: RoomId,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.add(
|
||||
earlierVersion,
|
||||
RoomIdTag.assemble(room.id, room.relayUrl.url),
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
): EphemeralChatListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(room.toTagArray()),
|
||||
tags = earlierVersion.tags.remove(room.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun resign(
|
||||
tags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
|
||||
tags = tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
tags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
): EphemeralChatListEvent {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
publicRooms: List<RoomId> = emptyList(),
|
||||
privateRooms: List<RoomId> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): EphemeralChatListEvent {
|
||||
val template = build(publicRooms, privateRooms, signer, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicRooms: List<RoomId> = emptyList(),
|
||||
privateRooms: List<RoomId> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<EphemeralChatListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<EphemeralChatListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip04(privateRooms.map { it.toTagArray() }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
rooms(publicRooms)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.ephemChat.list
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
@@ -27,3 +28,5 @@ fun TagArrayBuilder<EphemeralChatListEvent>.roomId(
|
||||
id: String,
|
||||
relayUrl: String,
|
||||
) = addUnique(RoomIdTag.assemble(id, relayUrl))
|
||||
|
||||
fun TagArrayBuilder<EphemeralChatListEvent>.rooms(rooms: List<RoomId>) = addAll(rooms.map { it.toTagArray() })
|
||||
|
||||
+28
@@ -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.experimental.ephemChat.list
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
fun TagArray.rooms() = mapNotNull(RoomIdTag::parse)
|
||||
|
||||
fun TagArray.roomSet() = mapNotNullTo(mutableSetOf(), RoomIdTag::parse)
|
||||
+24
-35
@@ -25,9 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Deprecated("Replaced by NIP-68")
|
||||
@Immutable
|
||||
class GalleryListEvent(
|
||||
id: HexKey,
|
||||
@@ -36,108 +37,96 @@ class GalleryListEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
companion object {
|
||||
const val KIND = 10011
|
||||
const val ALT = "Profile Gallery"
|
||||
const val GALLERYTAGNAME = "url"
|
||||
const val GALLERY_TAG_NAME = "url"
|
||||
|
||||
fun addEvent(
|
||||
suspend fun addEvent(
|
||||
earlierVersion: GalleryListEvent?,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
relay: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, GALLERYTAGNAME, eventId, url, relay, signer, createdAt, onReady)
|
||||
) = addTag(earlierVersion, GALLERY_TAG_NAME, eventId, url, relay, signer, createdAt)
|
||||
|
||||
fun addTag(
|
||||
suspend fun addTag(
|
||||
earlierVersion: GalleryListEvent?,
|
||||
tagName: String,
|
||||
eventid: HexKey,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
relay: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
val tags = arrayOf(tagName, url, eventid)
|
||||
): GalleryListEvent {
|
||||
val tags = arrayOf(tagName, url, eventId)
|
||||
if (relay != null) {
|
||||
tags + relay
|
||||
}
|
||||
|
||||
add(
|
||||
return add(
|
||||
earlierVersion,
|
||||
arrayOf(tags),
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun add(
|
||||
suspend fun add(
|
||||
earlierVersion: GalleryListEvent?,
|
||||
listNewTags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
): GalleryListEvent =
|
||||
create(
|
||||
content = earlierVersion?.content ?: "",
|
||||
tags = listNewTags.plus(earlierVersion?.tags ?: arrayOf()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun removeEvent(
|
||||
suspend fun removeEvent(
|
||||
earlierVersion: GalleryListEvent,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, GALLERYTAGNAME, eventId, url, signer, createdAt, onReady)
|
||||
) = removeTag(earlierVersion, GALLERY_TAG_NAME, eventId, url, signer, createdAt)
|
||||
|
||||
fun removeReplaceable(
|
||||
suspend fun removeReplaceable(
|
||||
earlierVersion: GalleryListEvent,
|
||||
aTag: ATag,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, GALLERYTAGNAME, aTag.toTag(), url, signer, createdAt, onReady)
|
||||
) = removeTag(earlierVersion, GALLERY_TAG_NAME, aTag.toTag(), url, signer, createdAt)
|
||||
|
||||
private fun removeTag(
|
||||
private suspend fun removeTag(
|
||||
earlierVersion: GalleryListEvent,
|
||||
tagName: String,
|
||||
eventid: HexKey,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
): GalleryListEvent =
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == url && it[2] == eventid) }
|
||||
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == url && it[2] == eventId) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
): GalleryListEvent {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
@@ -145,7 +134,7 @@ class GalleryListEvent(
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-10
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.experimental.relationshipStatus
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetnameTag
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetNameTag
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -45,7 +45,7 @@ class RelationshipStatusEvent(
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun rank() = tags.firstNotNullOfOrNull(RankTag::parse)
|
||||
|
||||
fun petname() = tags.firstNotNullOfOrNull(PetnameTag::parse)
|
||||
fun petName() = tags.firstNotNullOfOrNull(PetNameTag::parse)
|
||||
|
||||
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
|
||||
|
||||
@@ -53,16 +53,15 @@ class RelationshipStatusEvent(
|
||||
const val KIND = 30382
|
||||
const val ALT = "Relationship Status"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
targetUser: HexKey,
|
||||
petname: String? = null,
|
||||
petName: String? = null,
|
||||
summary: String? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
publicInitializer: TagArrayBuilder<RelationshipStatusEvent>.() -> Unit = {},
|
||||
privateInitializer: TagArrayBuilder<RelationshipStatusEvent>.() -> Unit = {},
|
||||
onReady: (RelationshipStatusEvent) -> Unit,
|
||||
) {
|
||||
): RelationshipStatusEvent {
|
||||
val publicTags =
|
||||
tagArray {
|
||||
alt(ALT)
|
||||
@@ -72,14 +71,13 @@ class RelationshipStatusEvent(
|
||||
|
||||
val privateTags =
|
||||
tagArray {
|
||||
petname?.let { petname(it) }
|
||||
petName?.let { petName(it) }
|
||||
summary?.let { summary(it) }
|
||||
privateInitializer()
|
||||
}
|
||||
|
||||
PrivateTagsInContent.encryptNip44(privateTags, signer) { content ->
|
||||
signer.sign(createdAt, KIND, publicTags, content, onReady)
|
||||
}
|
||||
val encryptedContent = PrivateTagsInContent.encryptNip44(privateTags, signer)
|
||||
return signer.sign(createdAt, KIND, publicTags, encryptedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,13 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.relationshipStatus
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetnameTag
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetNameTag
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
fun TagArrayBuilder<RelationshipStatusEvent>.rank(rank: Int) = add(RankTag.assemble(rank))
|
||||
|
||||
fun TagArrayBuilder<RelationshipStatusEvent>.petname(name: String) = add(PetnameTag.assemble(name))
|
||||
fun TagArrayBuilder<RelationshipStatusEvent>.petName(name: String) = add(PetNameTag.assemble(name))
|
||||
|
||||
fun TagArrayBuilder<RelationshipStatusEvent>.summary(summary: String) = add(SummaryTag.assemble(summary))
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.experimental.relationshipStatus.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class PetnameTag {
|
||||
class PetNameTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "petname"
|
||||
|
||||
@@ -36,6 +36,6 @@ class PetnameTag {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(petname: String) = arrayOf(TAG_NAME, petname)
|
||||
fun assemble(petName: String) = arrayOf(TAG_NAME, petName)
|
||||
}
|
||||
}
|
||||
@@ -41,17 +41,10 @@ fun Event.verifySignature(): Boolean {
|
||||
/** Checks if the ID is correct and then if the pubKey's secret key signed the event. */
|
||||
fun Event.checkSignature() {
|
||||
if (!verifyId()) {
|
||||
throw Exception(
|
||||
"""
|
||||
|Unexpected ID.
|
||||
| Event: ${toJson()}
|
||||
| Actual ID: $id
|
||||
| Generated: ${generateId()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
throw Exception("ID mismatch: our ID is ${generateId()} for event ${toJson()}")
|
||||
}
|
||||
if (!verifySignature()) {
|
||||
throw Exception("""Bad signature!""")
|
||||
throw Exception("Bad signature!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class EventAssembler {
|
||||
tags,
|
||||
content,
|
||||
sig,
|
||||
) as T
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer
|
||||
|
||||
class JsonMapper {
|
||||
companion object Companion {
|
||||
companion object {
|
||||
val defaultPrettyPrinter = InliningTagArrayPrettyPrinter()
|
||||
|
||||
val mapper =
|
||||
|
||||
+7
-2
@@ -24,10 +24,13 @@ import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RelayAuthenticator(
|
||||
val client: NostrClient,
|
||||
val authenticate: (challenge: String, relay: IRelayClient) -> Unit,
|
||||
val scope: CoroutineScope,
|
||||
val authenticate: suspend (challenge: String, relay: IRelayClient) -> Unit,
|
||||
) {
|
||||
private val clientListener =
|
||||
object : IRelayClientListener {
|
||||
@@ -35,7 +38,9 @@ class RelayAuthenticator(
|
||||
relay: IRelayClient,
|
||||
challenge: String,
|
||||
) {
|
||||
authenticate(challenge, relay)
|
||||
scope.launch {
|
||||
authenticate(challenge, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -144,7 +144,6 @@ open class BasicRelayClient(
|
||||
stats.newError(e.message ?: "Error trying to connect: ${e.javaClass.simpleName}")
|
||||
|
||||
markConnectionAsClosed()
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
connectingMutex.set(false)
|
||||
}
|
||||
@@ -224,7 +223,6 @@ open class BasicRelayClient(
|
||||
markConnectionAsClosed()
|
||||
|
||||
Log.w(logTag, "OnFailure $code $response ${t.message} $socket")
|
||||
t.printStackTrace()
|
||||
listener.onError(
|
||||
this@BasicRelayClient,
|
||||
"",
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.signers
|
||||
|
||||
import com.vitorpamplona.quartz.EventFactory
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
@@ -33,104 +31,49 @@ abstract class NostrSigner(
|
||||
) {
|
||||
abstract fun isWriteable(): Boolean
|
||||
|
||||
fun <T : Event> sign(
|
||||
ev: EventTemplate<T>,
|
||||
onReady: (T) -> Unit,
|
||||
) = sign(ev.createdAt, ev.kind, ev.tags, ev.content, onReady)
|
||||
suspend fun <T : Event> sign(ev: EventTemplate<T>): T = sign(ev.createdAt, ev.kind, ev.tags, ev.content)
|
||||
|
||||
abstract fun <T : Event> sign(
|
||||
abstract suspend fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
)
|
||||
): T
|
||||
|
||||
abstract fun nip04Encrypt(
|
||||
abstract suspend fun nip04Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
): String
|
||||
|
||||
abstract fun nip04Decrypt(
|
||||
abstract suspend fun nip04Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
): String
|
||||
|
||||
abstract fun nip44Encrypt(
|
||||
abstract suspend fun nip44Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
): String
|
||||
|
||||
abstract fun nip44Decrypt(
|
||||
abstract suspend fun nip44Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
): String
|
||||
|
||||
abstract fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
)
|
||||
abstract suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent
|
||||
|
||||
abstract fun deriveKey(
|
||||
nonce: HexKey,
|
||||
onReady: (HexKey) -> Unit,
|
||||
)
|
||||
abstract suspend fun deriveKey(nonce: HexKey): HexKey
|
||||
|
||||
fun decrypt(
|
||||
suspend fun decrypt(
|
||||
encryptedContent: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (EncryptedInfo.isNIP04(encryptedContent)) {
|
||||
nip04Decrypt(encryptedContent, fromPublicKey, onReady)
|
||||
): String {
|
||||
if (encryptedContent.isBlank()) throw SignerExceptions.NothingToDecrypt()
|
||||
|
||||
return if (EncryptedInfo.isNIP04(encryptedContent)) {
|
||||
nip04Decrypt(encryptedContent, fromPublicKey)
|
||||
} else {
|
||||
nip44Decrypt(encryptedContent, fromPublicKey, onReady)
|
||||
nip44Decrypt(encryptedContent, fromPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Event> assembleRumor(
|
||||
ev: EventTemplate<T>,
|
||||
onReady: (T) -> Unit,
|
||||
) = assembleRumor(ev.createdAt, ev.kind, ev.tags, ev.content, onReady)
|
||||
|
||||
fun <T : Event> assembleRumor(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
onReady(
|
||||
EventFactory.create(
|
||||
id = EventHasher.hashId(pubKey, createdAt, kind, tags, content),
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = content,
|
||||
sig = "",
|
||||
) as T,
|
||||
)
|
||||
}
|
||||
|
||||
fun <T : Event> assembleRumor(ev: EventTemplate<T>) = assembleRumor<T>(ev.createdAt, ev.kind, ev.tags, ev.content)
|
||||
|
||||
fun <T : Event> assembleRumor(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
) = EventFactory.create(
|
||||
id = EventHasher.hashId(pubKey, createdAt, kind, tags, content),
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = content,
|
||||
sig = "",
|
||||
) as T
|
||||
}
|
||||
|
||||
+47
-37
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class NostrSignerInternal(
|
||||
val keyPair: KeyPair,
|
||||
@@ -34,59 +35,68 @@ class NostrSignerInternal(
|
||||
|
||||
override fun isWriteable(): Boolean = keyPair.privKey != null
|
||||
|
||||
override fun <T : Event> sign(
|
||||
inline fun <T> runWrapErrors(action: () -> T): T =
|
||||
try {
|
||||
action()
|
||||
} catch (e: SignerExceptions) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw SignerExceptions.CouldNotPerformException("Could not sign event.", e)
|
||||
}
|
||||
|
||||
override suspend fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
signerSync.sign<T>(createdAt, kind, tags, content)?.let { onReady(it) }
|
||||
}
|
||||
): T =
|
||||
runWrapErrors {
|
||||
signerSync.sign<T>(createdAt, kind, tags, content)
|
||||
}
|
||||
|
||||
override fun nip04Encrypt(
|
||||
override suspend fun nip04Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip04Encrypt(plaintext, toPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
): String =
|
||||
runWrapErrors {
|
||||
signerSync.nip04Encrypt(plaintext, toPublicKey)
|
||||
}
|
||||
|
||||
override fun nip04Decrypt(
|
||||
override suspend fun nip04Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip04Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
): String =
|
||||
runWrapErrors {
|
||||
signerSync.nip04Decrypt(ciphertext, fromPublicKey)
|
||||
}
|
||||
|
||||
override fun nip44Encrypt(
|
||||
override suspend fun nip44Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip44Encrypt(plaintext, toPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
): String =
|
||||
runWrapErrors {
|
||||
signerSync.nip44Encrypt(plaintext, toPublicKey)
|
||||
}
|
||||
|
||||
override fun nip44Decrypt(
|
||||
override suspend fun nip44Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip44Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) }
|
||||
): String =
|
||||
runWrapErrors {
|
||||
signerSync.nip44Decrypt(ciphertext, fromPublicKey)
|
||||
}
|
||||
|
||||
override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent {
|
||||
if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt()
|
||||
|
||||
return runWrapErrors {
|
||||
signerSync.decryptZapEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
) {
|
||||
signerSync.decryptZapEvent(event)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun deriveKey(
|
||||
nonce: HexKey,
|
||||
onReady: (HexKey) -> Unit,
|
||||
) {
|
||||
signerSync.deriveKey(nonce)?.let { onReady(it) }
|
||||
}
|
||||
override suspend fun deriveKey(nonce: HexKey): HexKey =
|
||||
runWrapErrors {
|
||||
signerSync.deriveKey(nonce)
|
||||
}
|
||||
}
|
||||
|
||||
+36
-29
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.signers
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.experimental.decoupling.EncryptionKeyDerivation
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -28,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
|
||||
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
|
||||
import com.vitorpamplona.quartz.nip44Encryption.Nip44
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
@@ -45,8 +45,8 @@ class NostrSignerSync(
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T? {
|
||||
if (keyPair.privKey == null) return null
|
||||
): T {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
|
||||
return if (isUnsignedPrivateZapEvent(kind, tags)) {
|
||||
// this is a private zap
|
||||
@@ -68,71 +68,78 @@ class NostrSignerSync(
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T? {
|
||||
if (keyPair.privKey == null) return null
|
||||
): T {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
|
||||
return EventAssembler.hashAndSign<T>(pubKey, createdAt, kind, tags, content, keyPair.privKey)
|
||||
}
|
||||
|
||||
fun nip04Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
): String {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
|
||||
return Nip04.encrypt(
|
||||
decryptedContent,
|
||||
plaintext,
|
||||
keyPair.privKey,
|
||||
toPublicKey.hexToByteArray(),
|
||||
)
|
||||
}
|
||||
|
||||
fun nip04Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
): String {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
if (ciphertext.isBlank()) throw SignerExceptions.NothingToDecrypt()
|
||||
|
||||
return try {
|
||||
Nip04.decrypt(encryptedContent, keyPair.privKey, fromPublicKey.hexToByteArray())
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on $encryptedContent")
|
||||
null
|
||||
}
|
||||
return Nip04.decrypt(ciphertext, keyPair.privKey, fromPublicKey.hexToByteArray())
|
||||
}
|
||||
|
||||
fun nip44Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
): String {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
|
||||
return Nip44
|
||||
.encrypt(
|
||||
decryptedContent,
|
||||
plaintext,
|
||||
keyPair.privKey,
|
||||
toPublicKey.hexToByteArray(),
|
||||
).encodePayload()
|
||||
}
|
||||
|
||||
fun nip44Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String? {
|
||||
if (keyPair.privKey == null) return null
|
||||
): String {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
if (ciphertext.isBlank()) throw SignerExceptions.NothingToDecrypt()
|
||||
|
||||
return Nip44.decrypt(
|
||||
payload = encryptedContent,
|
||||
payload = ciphertext,
|
||||
privateKey = keyPair.privKey,
|
||||
pubKey = fromPublicKey.hexToByteArray(),
|
||||
)
|
||||
}
|
||||
|
||||
fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent? = PrivateZapRequestBuilder().decryptZapEvent(event, this)
|
||||
fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = PrivateZapRequestBuilder().decryptZapEvent(event, this)
|
||||
|
||||
fun deriveKey(nonce: HexKey): HexKey? {
|
||||
if (keyPair.privKey == null) return null
|
||||
fun deriveKey(nonce: HexKey): HexKey {
|
||||
if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException()
|
||||
|
||||
return EncryptionKeyDerivation.derivePrivateKey(keyPair.privKey, nonce.hexToByteArray()).toHexKey()
|
||||
}
|
||||
|
||||
suspend fun decrypt(
|
||||
encryptedContent: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String =
|
||||
if (EncryptedInfo.isNIP04(encryptedContent)) {
|
||||
nip04Decrypt(encryptedContent, fromPublicKey)
|
||||
} else {
|
||||
nip44Decrypt(encryptedContent, fromPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.nip01Core.signers
|
||||
|
||||
sealed class SignerExceptions(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : Exception(msg, cause) {
|
||||
class ReadOnlyException : SignerExceptions("Signer is read-only")
|
||||
|
||||
class UnauthorizedDecryptionException : SignerExceptions("Couldn't not decrypt the contents of this event.")
|
||||
|
||||
class NothingToDecrypt : SignerExceptions("Ciphertext is Empty")
|
||||
|
||||
class AutomaticallyUnauthorizedException(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : SignerExceptions(msg)
|
||||
|
||||
class ManuallyUnauthorizedException(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : SignerExceptions(msg)
|
||||
|
||||
class TimedOutException(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : SignerExceptions(msg)
|
||||
|
||||
class CouldNotPerformException(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : SignerExceptions(msg, cause)
|
||||
|
||||
class RunningOnBackgroundWithoutAutomaticPermissionException(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : SignerExceptions(msg)
|
||||
|
||||
class SignerNotFoundException(
|
||||
msg: String,
|
||||
cause: Throwable? = null,
|
||||
) : SignerExceptions(msg)
|
||||
}
|
||||
+37
@@ -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.nip01Core.signers.caches
|
||||
|
||||
sealed interface CacheResults<T> {
|
||||
class Success<T>(
|
||||
val value: T,
|
||||
) : CacheResults<T>
|
||||
|
||||
class DontTryAgain<T> : CacheResults<T>
|
||||
|
||||
class NeedsForegroundActivityToTryAgain<T>(
|
||||
val after: Long,
|
||||
) : CacheResults<T>
|
||||
|
||||
class CanTryAgain<T>(
|
||||
val after: Long,
|
||||
) : CacheResults<T>
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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.nip01Core.signers.caches
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
abstract class DecryptCache<I : Any, T : Any>(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
var cache: CacheResults<T> = CacheResults.CanTryAgain(0)
|
||||
|
||||
fun preload(result: T) {
|
||||
cache = CacheResults.Success(result)
|
||||
}
|
||||
|
||||
abstract suspend fun decryptAndParse(
|
||||
event: I,
|
||||
signer: NostrSigner,
|
||||
): T
|
||||
|
||||
private suspend fun performDecrypt(input: I): T? {
|
||||
try {
|
||||
val response = decryptAndParse(input, signer)
|
||||
cache = CacheResults.Success(response)
|
||||
return response
|
||||
} catch (e: SignerExceptions.NothingToDecrypt) {
|
||||
Log.w("DecryptCache", "Nothing to decrypt", e)
|
||||
// ciphertext is blank. Cancels everything.
|
||||
cache = CacheResults.DontTryAgain()
|
||||
} catch (e: SignerExceptions.AutomaticallyUnauthorizedException) {
|
||||
Log.w("DecryptCache", "NothAutomaticallyUnauthorizedException", e)
|
||||
// User has rejected this permission. Don't try again.
|
||||
cache = CacheResults.DontTryAgain<T>()
|
||||
} catch (e: SignerExceptions.ManuallyUnauthorizedException) {
|
||||
Log.w("DecryptCache", "ManuallyUnauthorizedException", e)
|
||||
// User has rejected this permission. Don't try again.
|
||||
cache = CacheResults.CanTryAgain<T>(TimeUtils.tenSecondsFromNow())
|
||||
} catch (e: SignerExceptions.TimedOutException) {
|
||||
Log.w("DecryptCache", "TimedOutException", e)
|
||||
// User has did not reply to the approval request. Ignore until later time.
|
||||
cache = CacheResults.CanTryAgain<T>(TimeUtils.tenSecondsFromNow())
|
||||
} catch (e: SignerExceptions.CouldNotPerformException) {
|
||||
Log.w("DecryptCache", "CouldNotPerformException", e)
|
||||
// Decryption failed. This key might not be able to decrypt anything. Don't try again.
|
||||
cache = CacheResults.DontTryAgain<T>()
|
||||
} catch (e: SignerExceptions.SignerNotFoundException) {
|
||||
Log.w("DecryptCache", "SignerNotFoundException", e)
|
||||
// Signer app was deleted. Not sure what to to. It should probably log off.
|
||||
cache = CacheResults.DontTryAgain<T>()
|
||||
} catch (e: SignerExceptions.RunningOnBackgroundWithoutAutomaticPermissionException) {
|
||||
Log.w("DecryptCache", "RunningOnBackgroundWithoutAutomaticPermissionException", e)
|
||||
// App received a notifications, asked the signer to decrypt but the permission was not automatic.
|
||||
// It needs the interface but does not have it. It needs to wait for an activity.
|
||||
cache = CacheResults.NeedsForegroundActivityToTryAgain<T>(TimeUtils.tenSecondsFromNow())
|
||||
} catch (e: com.fasterxml.jackson.core.JsonParseException) {
|
||||
Log.w("DecryptCache", "JsonParseException", e)
|
||||
// Decryption failed. This key might not be able to decrypt anything. Don't try again.
|
||||
cache = CacheResults.DontTryAgain()
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.w("DecryptCache", "IllegalStateException", e)
|
||||
cache = CacheResults.DontTryAgain()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w("DecryptCache", "IllegalArgumentException", e)
|
||||
cache = CacheResults.DontTryAgain()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun cached(): T? {
|
||||
val cachedResult = cache
|
||||
return if (cachedResult is CacheResults.Success<T>) {
|
||||
cachedResult.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decrypt(input: I): T? {
|
||||
val cachedResult = cache
|
||||
return when (cachedResult) {
|
||||
is CacheResults.Success<T> -> cachedResult.value
|
||||
is CacheResults.DontTryAgain -> null
|
||||
is CacheResults.CanTryAgain -> {
|
||||
if (TimeUtils.now() > cachedResult.after) {
|
||||
performDecrypt(input)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is CacheResults.NeedsForegroundActivityToTryAgain<*> -> {
|
||||
if (TimeUtils.now() > cachedResult.after && (signer !is NostrSignerExternal || signer.hasForegroundActivity())) {
|
||||
performDecrypt(input)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
data class ATag(
|
||||
val kind: Int,
|
||||
val pubKeyHex: HexKey,
|
||||
val dTag: String,
|
||||
val dTag: String = "",
|
||||
val relay: NormalizedRelayUrl? = null,
|
||||
) {
|
||||
constructor(address: Address, relayHint: NormalizedRelayUrl? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint)
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
data class Address(
|
||||
val kind: Int,
|
||||
val pubKeyHex: HexKey,
|
||||
val dTag: String,
|
||||
val dTag: String = "",
|
||||
) : Comparable<Address> {
|
||||
fun toValue() = assemble(kind, pubKeyHex, dTag)
|
||||
|
||||
@@ -59,7 +59,7 @@ data class Address(
|
||||
fun assemble(
|
||||
kind: Int,
|
||||
pubKeyHex: HexKey,
|
||||
dTag: String,
|
||||
dTag: String = "",
|
||||
) = "$kind:$pubKeyHex:$dTag"
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -94,12 +94,12 @@ class ContactListEvent(
|
||||
|
||||
fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:"
|
||||
|
||||
fun createFromScratch(
|
||||
suspend fun createFromScratch(
|
||||
followUsers: List<ContactTag> = emptyList(),
|
||||
relayUse: Map<String, ReadWrite>? = emptyMap(),
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ContactListEvent? {
|
||||
): ContactListEvent {
|
||||
val content = relayUse?.let { RelaySet.assemble(it) } ?: ""
|
||||
|
||||
val tags =
|
||||
@@ -109,13 +109,12 @@ class ContactListEvent(
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
suspend fun createFromScratch(
|
||||
followUsers: List<ContactTag>,
|
||||
relayUse: Map<String, ReadWrite>?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
): ContactListEvent {
|
||||
val content = relayUse?.let { RelaySet.assemble(it) } ?: ""
|
||||
|
||||
val tags = followUsers.map { it.toTagArray() }
|
||||
@@ -124,53 +123,47 @@ class ContactListEvent(
|
||||
tags = tags.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun followUser(
|
||||
suspend fun followUser(
|
||||
earlierVersion: ContactListEvent,
|
||||
pubKeyHex: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex)) return
|
||||
): ContactListEvent {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = arrayOf("p", pubKeyHex)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowUser(
|
||||
suspend fun unfollowUser(
|
||||
earlierVersion: ContactListEvent,
|
||||
pubKeyHex: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex)) return
|
||||
): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun updateRelayList(
|
||||
suspend fun updateRelayList(
|
||||
earlierVersion: ContactListEvent,
|
||||
relayUse: Map<String, ReadWrite>?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
): ContactListEvent {
|
||||
val content = relayUse?.let { RelaySet.assemble(it) } ?: ""
|
||||
|
||||
return create(
|
||||
@@ -178,17 +171,15 @@ class ContactListEvent(
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
): ContactListEvent {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
@@ -196,7 +187,7 @@ class ContactListEvent(
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.nip04Dm
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
|
||||
class PrivateDMCache(
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
private val decryptionCache =
|
||||
object : LruCache<PrivateDmEvent, PrivateDMDecryptCache>(10000) {
|
||||
override fun create(key: PrivateDmEvent): PrivateDMDecryptCache? {
|
||||
val canDecrypt = key.canDecrypt(signer.pubKey)
|
||||
return if (key.content.isNotBlank() && canDecrypt) {
|
||||
PrivateDMDecryptCache(signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cachedDM(event: PrivateDmEvent): String? = decryptionCache[event]?.cached()
|
||||
|
||||
suspend fun decryptDM(event: PrivateDmEvent) = decryptionCache[event]?.decrypt(event)
|
||||
}
|
||||
|
||||
class PrivateDMDecryptCache(
|
||||
signer: NostrSigner,
|
||||
) : DecryptCache<PrivateDmEvent, String>(signer) {
|
||||
override suspend fun decryptAndParse(
|
||||
event: PrivateDmEvent,
|
||||
signer: NostrSigner,
|
||||
) = event.decryptContent(signer)
|
||||
}
|
||||
@@ -26,7 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.any
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.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.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
|
||||
@@ -37,8 +39,6 @@ import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
@Immutable
|
||||
@@ -50,21 +50,35 @@ class PrivateDmEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
ChatroomKeyable {
|
||||
@Transient private var decryptedContent: Map<HexKey, String> = mapOf()
|
||||
ChatroomKeyable,
|
||||
PubKeyHintProvider {
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (decryptedContent.values.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() })
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun canDecrypt(signer: NostrSigner) = canDecrypt(signer.pubKey)
|
||||
|
||||
fun canDecrypt(signerPubKey: HexKey) = pubKey == signerPubKey || recipientPubKey() == signerPubKey
|
||||
|
||||
suspend fun decryptContent(signer: NostrSigner): String {
|
||||
if (!canDecrypt(signer.pubKey)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
val retVal = signer.decrypt(content, talkingWith(signer.pubKey))
|
||||
return if (retVal.startsWith(NIP_18_ADVERTISEMENT)) {
|
||||
retVal.substring(16)
|
||||
} else {
|
||||
retVal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This may or may not be the actual recipient's pub key. The event is intended to look like a
|
||||
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used for
|
||||
* initial messages.
|
||||
*/
|
||||
private fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey)
|
||||
fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey)
|
||||
|
||||
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull()
|
||||
|
||||
@@ -79,43 +93,12 @@ class PrivateDmEvent(
|
||||
|
||||
fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey
|
||||
|
||||
override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(persistentSetOf(talkingWith(toRemove)))
|
||||
override fun chatroomKey(toRemove: HexKey): ChatroomKey = ChatroomKey(persistentSetOf(talkingWith(toRemove)))
|
||||
|
||||
/**
|
||||
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
|
||||
*
|
||||
* Nip-18 messages should refer to other events by inline references in the content like
|
||||
* `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506).
|
||||
*/
|
||||
fun replyTo() = tags.firstNotNullOfOrNull(MarkedETag::parseId)
|
||||
|
||||
fun with(pubkeyHex: HexKey): Boolean = pubkeyHex == pubKey || tags.any(PTag::isTagged, pubkeyHex)
|
||||
|
||||
fun cachedContentFor(signer: NostrSigner): String? = decryptedContent[signer.pubKey]
|
||||
|
||||
fun plainContent(
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
decryptedContent[signer.pubKey]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
signer.decrypt(content, talkingWith(signer.pubKey)) { retVal ->
|
||||
val content =
|
||||
if (retVal.startsWith(NIP_18_ADVERTISEMENT)) {
|
||||
retVal.substring(16)
|
||||
} else {
|
||||
retVal
|
||||
}
|
||||
|
||||
decryptedContent = decryptedContent + Pair(signer.pubKey, content)
|
||||
|
||||
onReady(content)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 4
|
||||
const val ALT = "Private Message"
|
||||
@@ -123,11 +106,11 @@ class PrivateDmEvent(
|
||||
|
||||
fun prepareMessageToEncrypt(
|
||||
msg: String,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
iMetas: List<IMetaTag>? = null,
|
||||
advertiseNip18: Boolean = true,
|
||||
): String {
|
||||
var message = msg
|
||||
imetas?.forEach {
|
||||
iMetas?.forEach {
|
||||
message = message.replace(it.url, Nip54InlineMetadata().createUrl(it.url, it.properties))
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -48,7 +48,9 @@ class Nip11RelayInformation(
|
||||
) {
|
||||
companion object {
|
||||
val mapper =
|
||||
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
jacksonObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
|
||||
|
||||
fun fromJson(json: String): Nip11RelayInformation = mapper.readValue(json, Nip11RelayInformation::class.java)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.utils.mapNotNullAsync
|
||||
|
||||
class NIP17Factory {
|
||||
data class Result(
|
||||
@@ -38,122 +39,82 @@ class NIP17Factory {
|
||||
val wraps: List<GiftWrapEvent>,
|
||||
)
|
||||
|
||||
private fun recursiveGiftWrapCreation(
|
||||
event: Event,
|
||||
remainingTos: List<HexKey>,
|
||||
signer: NostrSigner,
|
||||
output: MutableList<GiftWrapEvent>,
|
||||
onReady: (List<GiftWrapEvent>) -> Unit,
|
||||
) {
|
||||
if (remainingTos.isEmpty()) {
|
||||
onReady(output)
|
||||
return
|
||||
}
|
||||
|
||||
val next = remainingTos.first()
|
||||
|
||||
SealedRumorEvent.create(
|
||||
event = event,
|
||||
encryptTo = next,
|
||||
signer = signer,
|
||||
) { seal ->
|
||||
GiftWrapEvent.create(
|
||||
event = seal,
|
||||
recipientPubKey = next,
|
||||
) { giftWrap ->
|
||||
output.add(giftWrap)
|
||||
recursiveGiftWrapCreation(event, remainingTos.minus(next), signer, output, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWraps(
|
||||
private suspend fun createWraps(
|
||||
event: Event,
|
||||
to: Set<HexKey>,
|
||||
signer: NostrSigner,
|
||||
onReady: (List<GiftWrapEvent>) -> Unit,
|
||||
) {
|
||||
val wraps = mutableListOf<GiftWrapEvent>()
|
||||
recursiveGiftWrapCreation(event, to.toList(), signer, wraps, onReady)
|
||||
}
|
||||
): List<GiftWrapEvent> =
|
||||
mapNotNullAsync(
|
||||
to.toList(),
|
||||
) { next ->
|
||||
GiftWrapEvent.create(
|
||||
event =
|
||||
SealedRumorEvent.create(
|
||||
event = event,
|
||||
encryptTo = next,
|
||||
signer = signer,
|
||||
),
|
||||
recipientPubKey = next,
|
||||
)
|
||||
}
|
||||
|
||||
fun createMessageNIP17(
|
||||
suspend fun createMessageNIP17(
|
||||
template: EventTemplate<ChatMessageEvent>,
|
||||
signer: NostrSigner,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
signer.sign(template) { senderMessage ->
|
||||
createWraps(senderMessage, senderMessage.groupMembers(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
): Result {
|
||||
val senderMessage = signer.sign(template)
|
||||
val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer)
|
||||
return Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
)
|
||||
}
|
||||
|
||||
fun createEncryptedFileNIP17(
|
||||
suspend fun createEncryptedFileNIP17(
|
||||
template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>,
|
||||
signer: NostrSigner,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
signer.sign(template) { senderMessage ->
|
||||
createWraps(senderMessage, senderMessage.groupMembers(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
): Result {
|
||||
val senderMessage = signer.sign(template)
|
||||
val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer)
|
||||
|
||||
return Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
)
|
||||
}
|
||||
|
||||
fun createReactionWithinGroup(
|
||||
suspend fun createReactionWithinGroup(
|
||||
content: String,
|
||||
originalNote: EventHintBundle<Event>,
|
||||
to: List<HexKey>,
|
||||
signer: NostrSigner,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
): Result {
|
||||
val senderPublicKey = signer.pubKey
|
||||
val template = ReactionEvent.build(content, originalNote)
|
||||
|
||||
signer.sign(
|
||||
ReactionEvent.build(content, originalNote),
|
||||
) { senderReaction ->
|
||||
createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderReaction,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val senderReaction = signer.sign(template)
|
||||
val wraps = createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer)
|
||||
return Result(
|
||||
msg = senderReaction,
|
||||
wraps = wraps,
|
||||
)
|
||||
}
|
||||
|
||||
fun createReactionWithinGroup(
|
||||
suspend fun createReactionWithinGroup(
|
||||
emojiUrl: EmojiUrlTag,
|
||||
originalNote: EventHintBundle<Event>,
|
||||
to: List<HexKey>,
|
||||
signer: NostrSigner,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
): Result {
|
||||
val senderPublicKey = signer.pubKey
|
||||
val template = ReactionEvent.build(emojiUrl, originalNote)
|
||||
|
||||
signer.sign(
|
||||
ReactionEvent.build(emojiUrl, originalNote),
|
||||
) { senderReaction ->
|
||||
createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderReaction,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val senderReaction = signer.sign(template)
|
||||
val wraps = createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer)
|
||||
|
||||
return Result(
|
||||
msg = senderReaction,
|
||||
wraps = wraps,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip17Dm.base
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.any
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
@@ -68,6 +69,8 @@ open class BaseDMGroupEvent(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun isIncluded(pubKey: HexKey) = tags.any(PTag::isTagged, pubKey)
|
||||
|
||||
override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet()
|
||||
|
||||
override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(talkingWith(toRemove).toImmutableSet())
|
||||
|
||||
@@ -26,4 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@Stable
|
||||
data class ChatroomKey(
|
||||
val users: Set<HexKey>,
|
||||
)
|
||||
) : Comparable<ChatroomKey> {
|
||||
override fun compareTo(other: ChatroomKey): Int = users.hashCode().compareTo(other.users.hashCode())
|
||||
}
|
||||
|
||||
@@ -23,5 +23,7 @@ package com.vitorpamplona.quartz.nip17Dm.base
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
interface NIP17Group {
|
||||
fun isIncluded(pubKey: HexKey): Boolean
|
||||
|
||||
fun groupMembers(): Set<HexKey>
|
||||
}
|
||||
|
||||
+8
-22
@@ -54,19 +54,17 @@ class ChatMessageRelayListEvent(
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.plusElement(
|
||||
.map { RelayTag.assemble(it) }
|
||||
.plusElement(
|
||||
AltTag.assemble("Relay list to receive private messages"),
|
||||
).toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
suspend fun updateRelayList(
|
||||
earlierVersion: ChatMessageRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChatMessageRelayListEvent) -> Unit,
|
||||
) {
|
||||
): ChatMessageRelayListEvent {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter(RelayTag::notMatch)
|
||||
@@ -76,31 +74,19 @@ class ChatMessageRelayListEvent(
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
return signer.sign(createdAt, KIND, tags, earlierVersion.content)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
suspend fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChatMessageRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChatMessageRelayListEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
): ChatMessageRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ChatMessageRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
): ChatMessageRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,12 +110,11 @@ class GenericRepostEvent(
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
boostedPost: Event,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GenericRepostEvent) -> Unit,
|
||||
) {
|
||||
): GenericRepostEvent {
|
||||
val content = boostedPost.toJson()
|
||||
|
||||
val tags =
|
||||
@@ -131,7 +130,7 @@ class GenericRepostEvent(
|
||||
tags.add(arrayOf("k", "${boostedPost.kind}"))
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ data class NAddress(
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("NAddress", "Issue trying to Decode NIP19 $this: ${e.message}")
|
||||
// e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNote
|
||||
data class NNote(
|
||||
val hex: String,
|
||||
) : Entity {
|
||||
companion object Companion {
|
||||
companion object {
|
||||
fun parse(bytes: ByteArray): NNote? {
|
||||
if (bytes.isEmpty()) return null
|
||||
return NNote(bytes.toHexKey())
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ class ReplyIdentifierTag {
|
||||
fun matchOrNull(
|
||||
tag: Array<String>,
|
||||
encodedScope: Set<String>,
|
||||
) = if (tag.has(1) && tag[0] == RootIdentifierTag.Companion.TAG_NAME && tag[1] in encodedScope) {
|
||||
) = if (tag.has(1) && tag[0] == TAG_NAME && tag[1] in encodedScope) {
|
||||
tag[1]
|
||||
} else {
|
||||
null
|
||||
@@ -93,7 +93,7 @@ class ReplyIdentifierTag {
|
||||
@JvmStatic
|
||||
fun parseExternalId(tag: Tag): ExternalId? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == RootIdentifierTag.Companion.TAG_NAME) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
val value = tag[1]
|
||||
|
||||
+131
-151
@@ -22,17 +22,23 @@ package com.vitorpamplona.quartz.nip28PublicChat.list
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional
|
||||
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.NostrSignerSync
|
||||
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.events.ETag
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@@ -43,24 +49,11 @@ class ChannelListEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var publicAndPrivateEventCache: Set<EventIdHint>? = null
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider {
|
||||
override fun eventHints() = tags.mapNotNull(ChannelTag::parseAsHint)
|
||||
|
||||
fun publicAndPrivateChannels(
|
||||
signer: NostrSigner,
|
||||
onReady: (Set<EventIdHint>) -> Unit,
|
||||
) {
|
||||
publicAndPrivateEventCache?.let { eventList ->
|
||||
onReady(eventList)
|
||||
return
|
||||
}
|
||||
|
||||
mergeTagList(signer) {
|
||||
val set = it.mapNotNull(ETag::parseAsHint).toSet()
|
||||
publicAndPrivateEventCache = set
|
||||
onReady(set)
|
||||
}
|
||||
}
|
||||
override fun linkedEventIds() = tags.mapNotNull(ChannelTag::parseId)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10005
|
||||
@@ -69,170 +62,157 @@ class ChannelListEvent(
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
private fun createChannelBase(
|
||||
tags: Array<Array<String>>,
|
||||
suspend fun create(
|
||||
channel: ChannelTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.create(
|
||||
tags,
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun createChannel(
|
||||
channel: EventHintBundle<ChannelCreateEvent>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) = createChannelBase(
|
||||
tags = arrayOf(ETag.assemble(channel.event.id, channel.relay, channel.event.pubKey)),
|
||||
) = create(
|
||||
channels = listOf(channel),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun createChannel(
|
||||
channel: EventIdHintOptional,
|
||||
suspend fun create(
|
||||
channels: List<ChannelTag>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) = createChannelBase(
|
||||
tags = arrayOf(ETag.assemble(channel.eventId, channel.relay, null)),
|
||||
): ChannelListEvent =
|
||||
if (isPrivate) {
|
||||
create(
|
||||
publicChannels = emptyList(),
|
||||
privateChannels = channels,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
publicChannels = channels,
|
||||
privateChannels = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channel: ChannelTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = add(
|
||||
earlierVersion = earlierVersion,
|
||||
channels = listOf(channel),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun createChannels(
|
||||
channels: List<EventIdHintOptional>,
|
||||
suspend fun add(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channels: List<ChannelTag>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) = createChannelBase(
|
||||
tags = channels.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray(),
|
||||
isPrivate = isPrivate,
|
||||
): ChannelListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
tags = earlierVersion.tags,
|
||||
privateTags = privateTags.removeAny(channels.map { it.toTagIdOnly() }) + channels.map { it.toTagArray() },
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.removeAny(channels.map { it.toTagIdOnly() }) + channels.map { it.toTagArray() },
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channel: ChannelTag,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ChannelListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(channel.toTagArray()),
|
||||
tags = earlierVersion.tags.remove(channel.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
tags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
|
||||
tags = tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun removeChannel(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channel: HexKey,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.removeAll(
|
||||
earlierVersion,
|
||||
ETag.assemble(channel, null, null),
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addChannelBase(
|
||||
earlierVersion: ChannelListEvent,
|
||||
newTags: Array<Array<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.addAll(
|
||||
earlierVersion,
|
||||
newTags,
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun addChannel(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channel: EventHintBundle<ChannelCreateEvent>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) = addChannelBase(
|
||||
earlierVersion,
|
||||
arrayOf(ETag.assemble(channel.event.id, channel.relay, channel.event.pubKey)),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
fun addChannel(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channel: EventIdHintOptional,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) = addChannelBase(
|
||||
earlierVersion,
|
||||
arrayOf(ETag.assemble(channel.eventId, channel.relay, null)),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
fun addChannels(
|
||||
earlierVersion: ChannelListEvent,
|
||||
channels: List<EventIdHintOptional>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) = addChannelBase(
|
||||
earlierVersion,
|
||||
channels.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray(),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
private fun create(
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
tags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
): ChannelListEvent {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.Companion.assemble(ALT)
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
publicChannels: List<ChannelTag> = emptyList(),
|
||||
privateChannels: List<ChannelTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ChannelListEvent {
|
||||
val template = build(publicChannels, privateChannels, signer, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
fun create(
|
||||
list: List<EventIdHint>,
|
||||
publicChannels: List<ChannelTag> = emptyList(),
|
||||
privateChannels: List<ChannelTag> = emptyList(),
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): ChannelListEvent? {
|
||||
val tags = list.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray()
|
||||
return signer.sign(createdAt, KIND, tags, "")
|
||||
): ChannelListEvent {
|
||||
val privateTagArray = privateChannels.map { it.toTagArray() }.toTypedArray()
|
||||
val publicTagArray = publicChannels.map { it.toTagArray() }.toTypedArray() + AltTag.assemble(ALT)
|
||||
return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicChannels: List<ChannelTag> = emptyList(),
|
||||
privateChannels: List<ChannelTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<ChannelListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<ChannelListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip04(privateChannels.map { it.toTagArray() }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
channels(publicChannels)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -24,8 +24,11 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
|
||||
fun TagArrayBuilder<ChannelListEvent>.followChat(
|
||||
eventId: HexKey,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
) = addUnique(ETag.assemble(eventId, relayUrl, null))
|
||||
|
||||
fun TagArrayBuilder<ChannelListEvent>.channels(rooms: List<ChannelTag>) = addAll(rooms.map { it.toTagArray() })
|
||||
|
||||
@@ -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.nip28PublicChat.list
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
|
||||
fun TagArray.channels() = mapNotNull(ChannelTag::parse)
|
||||
|
||||
fun TagArray.channelSet() = mapNotNullTo(mutableSetOf(), ChannelTag::parse)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.nip28PublicChat.list.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class ChannelTag(
|
||||
val eventId: HexKey,
|
||||
val relay: NormalizedRelayUrl? = null,
|
||||
val author: HexKey? = null,
|
||||
) {
|
||||
fun countMemory(): Long =
|
||||
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
|
||||
eventId.bytesUsedInMemory() +
|
||||
(relay?.url?.bytesUsedInMemory() ?: 0) +
|
||||
(author?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toNEvent(): String = NEvent.create(eventId, author, null, relay)
|
||||
|
||||
fun toTagArray() = assemble(eventId, relay, author)
|
||||
|
||||
fun toTagIdOnly() = assemble(eventId, null, null)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "e"
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
eventId: HexKey,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): ChannelTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
return ChannelTag(tag[1], pickRelayHint(tag), pickAuthor(tag))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun pickRelayHint(tag: Array<String>): NormalizedRelayUrl? {
|
||||
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3])
|
||||
if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4])
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun pickAuthor(tag: Array<String>): HexKey? {
|
||||
if (tag.has(2) && tag[2].length == 64) return tag[2]
|
||||
if (tag.has(3) && tag[3].length == 64) return tag[3]
|
||||
if (tag.has(4) && tag[4].length == 64) return tag[4]
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseAsHint(tag: Array<String>): EventIdHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val hint = pickRelayHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return EventIdHint(tag[1], hint)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
author: HexKey?,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.name
|
||||
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.util.UUID
|
||||
|
||||
@@ -40,7 +40,7 @@ class EmojiPackEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
companion object {
|
||||
const val KIND = 30030
|
||||
const val ALT_DESCRIPTION = "Emoji pack"
|
||||
|
||||
@@ -27,6 +27,9 @@ class AltTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "alt"
|
||||
|
||||
@JvmStatic
|
||||
fun match(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
|
||||
@@ -107,12 +107,11 @@ class GitPatchEvent(
|
||||
const val KIND = 1617
|
||||
const val ALT = "A Git Patch"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
patch: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
signer: NostrSigner,
|
||||
onReady: (GitPatchEvent) -> Unit,
|
||||
) {
|
||||
): GitPatchEvent {
|
||||
val content = patch
|
||||
val tags =
|
||||
mutableListOf(
|
||||
@@ -121,7 +120,7 @@ class GitPatchEvent(
|
||||
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,30 +30,25 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class DraftBuilder {
|
||||
companion object {
|
||||
fun <T : Event> encryptAndSign(
|
||||
suspend fun <T : Event> encryptAndSign(
|
||||
dTag: String,
|
||||
draft: T,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
signer.nip44Encrypt(draft.toJson(), signer.pubKey) { encryptedContent ->
|
||||
val template =
|
||||
eventTemplate<DraftEvent>(DraftEvent.KIND, encryptedContent, createdAt) {
|
||||
alt(DraftEvent.ALT_DESCRIPTION)
|
||||
dTag(dTag)
|
||||
kind(draft.kind)
|
||||
): DraftEvent {
|
||||
val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey)
|
||||
val template =
|
||||
eventTemplate<DraftEvent>(DraftEvent.KIND, encryptedContent, createdAt) {
|
||||
alt(DraftEvent.ALT_DESCRIPTION)
|
||||
dTag(dTag)
|
||||
kind(draft.kind)
|
||||
|
||||
if (draft is ExposeInDraft) {
|
||||
addAll(draft.exposeInDraft())
|
||||
}
|
||||
if (draft is ExposeInDraft) {
|
||||
addAll(draft.exposeInDraft())
|
||||
}
|
||||
|
||||
signer.sign(template) {
|
||||
it.addToCache(signer.pubKey, draft)
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
|
||||
return signer.sign(template)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
*/
|
||||
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
|
||||
@@ -30,6 +32,7 @@ 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
|
||||
@@ -41,7 +44,6 @@ 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
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class DraftEvent(
|
||||
@@ -67,76 +69,23 @@ class DraftEvent(
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
32 + (cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) })
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun isDeleted() = content == ""
|
||||
|
||||
fun preCachedDraft(signer: NostrSigner): Event? = cachedInnerEvent[signer.pubKey]
|
||||
fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey
|
||||
|
||||
fun preCachedDraft(pubKey: HexKey): Event? = cachedInnerEvent[pubKey]
|
||||
suspend fun createDeletedEvent(signer: NostrSigner): DraftEvent = signer.sign(createdAt, KIND, tags, "")
|
||||
|
||||
fun allCache() = cachedInnerEvent.values
|
||||
suspend fun decryptInnerEvent(signer: NostrSigner): Event {
|
||||
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
fun addToCache(
|
||||
pubKey: HexKey,
|
||||
innerEvent: Event,
|
||||
) {
|
||||
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, innerEvent)
|
||||
}
|
||||
|
||||
fun cachedDraft(
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
cachedInnerEvent[signer.pubKey]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
decrypt(signer) { draft ->
|
||||
addToCache(signer.pubKey, draft)
|
||||
|
||||
onReady(draft)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decrypt(
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
try {
|
||||
plainContent(signer) {
|
||||
try {
|
||||
onReady(fromJson(it))
|
||||
} catch (e: Exception) {
|
||||
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun plainContent(
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
signer.nip44Decrypt(content, pubKey, onReady)
|
||||
}
|
||||
|
||||
fun createDeletedEvent(
|
||||
signer: NostrSigner,
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
signer.sign<DraftEvent>(createdAt, KIND, tags, "") {
|
||||
onReady(it)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,123 +98,116 @@ class DraftEvent(
|
||||
dTag: String,
|
||||
): String = Address.assemble(KIND, pubKey, dTag)
|
||||
|
||||
fun create(
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: TorrentCommentEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags.filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: InteractiveStoryBaseEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: LiveActivitiesChatMessageEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): 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")) }
|
||||
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: ChannelMessageEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.channelId()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: GitReplyEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) }
|
||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: PollNoteEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags.filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: CommentEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers = originalNote.rootScopes() + originalNote.directReplies()
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: TextNoteEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags.filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
innerEvent: Event,
|
||||
anchorTagArray: List<Array<String>> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
tags.add(arrayOf("d", dTag))
|
||||
tags.add(arrayOf("k", "${innerEvent.kind}"))
|
||||
@@ -274,12 +216,15 @@ class DraftEvent(
|
||||
tags.addAll(anchorTagArray)
|
||||
}
|
||||
|
||||
signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey) { encryptedContent ->
|
||||
signer.sign<DraftEvent>(createdAt, KIND, tags.toTypedArray(), encryptedContent) {
|
||||
it.addToCache(signer.pubKey, innerEvent)
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
val draft =
|
||||
signer.sign<DraftEvent>(
|
||||
createdAt = createdAt,
|
||||
kind = KIND,
|
||||
tags = tags.toTypedArray(),
|
||||
content = signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey),
|
||||
)
|
||||
|
||||
return draft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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 androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
|
||||
class DraftEventCache(
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
private val decryptionCache =
|
||||
object : LruCache<DraftEvent, DraftEventDecryptCache>(1000) {
|
||||
override fun create(key: DraftEvent): DraftEventDecryptCache? =
|
||||
if (!key.isDeleted() && key.pubKey == signer.pubKey) {
|
||||
DraftEventDecryptCache(signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(event: DraftEvent) = decryptionCache.remove(event)
|
||||
|
||||
fun preload(
|
||||
event: DraftEvent,
|
||||
result: Event,
|
||||
) = decryptionCache[event]?.preload(result)
|
||||
|
||||
fun preCachedDraft(event: DraftEvent): Event? = decryptionCache[event]?.cached()
|
||||
|
||||
suspend fun cachedDraft(event: DraftEvent) = decryptionCache[event]?.decrypt(event)
|
||||
}
|
||||
|
||||
class DraftEventDecryptCache(
|
||||
signer: NostrSigner,
|
||||
) : DecryptCache<DraftEvent, Event>(signer) {
|
||||
override suspend fun decryptAndParse(
|
||||
event: DraftEvent,
|
||||
signer: NostrSigner,
|
||||
): Event = event.decryptInnerEvent(signer)
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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.privateOutbox
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
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.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.privateRelays
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class PrivateOutboxRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun publicRelays() = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
suspend fun privateRelays(signer: NostrSigner) = privateTags(signer)?.mapNotNull(RelayTag::parse)
|
||||
|
||||
suspend fun relays(signer: NostrSigner): List<NormalizedRelayUrl> = publicRelays() + (privateRelays(signer) ?: emptyList())
|
||||
|
||||
companion object {
|
||||
const val KIND = 10013
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
val ALT = "Relay list to store private content from this author"
|
||||
val TAGS = arrayOf(AltTag.assemble(ALT))
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
suspend fun updateRelayList(
|
||||
earlierVersion: PrivateOutboxRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): PrivateOutboxRelayListEvent {
|
||||
val newRelayList = relays.map { RelayTag.assemble(it) }
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
val publicTags = earlierVersion.tags.remove(RelayTag::match)
|
||||
val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList)
|
||||
|
||||
return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): PrivateOutboxRelayListEvent {
|
||||
val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray()
|
||||
return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): PrivateOutboxRelayListEvent {
|
||||
val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray()
|
||||
return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicRelays: List<NormalizedRelayUrl> = emptyList(),
|
||||
privateRelays: List<NormalizedRelayUrl> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<PrivateOutboxRelayListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<PrivateOutboxRelayListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
privateRelays(publicRelays)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,42 +41,39 @@ class StatusEvent(
|
||||
companion object {
|
||||
const val KIND = 30315
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
msg: String,
|
||||
type: String,
|
||||
expiration: Long?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (StatusEvent) -> Unit,
|
||||
) {
|
||||
): StatusEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
tags.add(arrayOf("d", type))
|
||||
expiration?.let { tags.add(arrayOf("expiration", it.toString())) }
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), msg)
|
||||
}
|
||||
|
||||
fun update(
|
||||
suspend fun update(
|
||||
event: StatusEvent,
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (StatusEvent) -> Unit,
|
||||
) {
|
||||
): StatusEvent {
|
||||
val tags = event.tags
|
||||
signer.sign(createdAt, KIND, tags, newStatus, onReady)
|
||||
return signer.sign(createdAt, KIND, tags, newStatus)
|
||||
}
|
||||
|
||||
fun clear(
|
||||
suspend fun clear(
|
||||
event: StatusEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (StatusEvent) -> Unit,
|
||||
) {
|
||||
): StatusEvent {
|
||||
val msg = ""
|
||||
val tags = event.tags.filter { it.size > 1 && it[0] == "d" }
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,36 +45,34 @@ class RelayAuthEvent(
|
||||
companion object {
|
||||
const val KIND = 22242
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
relay: NormalizedRelayUrl,
|
||||
challenge: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (RelayAuthEvent) -> Unit,
|
||||
) {
|
||||
): RelayAuthEvent {
|
||||
val content = ""
|
||||
val tags =
|
||||
arrayOf(
|
||||
RelayTag.assemble(relay),
|
||||
ChallengeTag.assemble(challenge),
|
||||
)
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, tags, content)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
challenge: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (RelayAuthEvent) -> Unit,
|
||||
) {
|
||||
): RelayAuthEvent {
|
||||
val content = ""
|
||||
val tags =
|
||||
relays
|
||||
.map { RelayTag.assemble(it) }
|
||||
.plusElement(ChallengeTag.assemble(challenge))
|
||||
.toTypedArray()
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, tags, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip44Encryption
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
|
||||
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
|
||||
@@ -49,8 +48,8 @@ object Nip44 {
|
||||
payload: String,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? {
|
||||
if (payload.isEmpty()) return null
|
||||
): String {
|
||||
require(payload.isNotBlank()) { "Payload must not be blank" }
|
||||
return if (payload[0] == '{') {
|
||||
decryptNIP44FromJackson(payload, privateKey, pubKey)
|
||||
} else {
|
||||
@@ -62,14 +61,13 @@ object Nip44 {
|
||||
json: String,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? {
|
||||
): String {
|
||||
// Ignores if it is not a valid json
|
||||
val info =
|
||||
try {
|
||||
JsonMapper.mapper.readValue(json, EncryptedInfoString::class.java)
|
||||
} catch (e: Exception) {
|
||||
Log.e("NIP44", "Unable to parse json $json")
|
||||
return null
|
||||
throw IllegalArgumentException("Unable to parse NIP-44 JSON: $json")
|
||||
}
|
||||
|
||||
return when (info.v) {
|
||||
@@ -101,31 +99,25 @@ object Nip44 {
|
||||
v2.decrypt(encryptedInfo, privateKey, pubKey)
|
||||
}
|
||||
|
||||
else -> null
|
||||
else -> throw IllegalArgumentException("Invalid or unsupported NIP-44 version code ${info.v}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun decryptNIP44FromBase64(
|
||||
payload: String,
|
||||
ciphertext: String,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? {
|
||||
if (payload.isEmpty()) return null
|
||||
): String {
|
||||
require(ciphertext.isNotBlank()) { "ciphertext must not be blank" }
|
||||
|
||||
// Ignores if it is not base64
|
||||
val byteArray =
|
||||
try {
|
||||
Base64.getDecoder().decode(payload)
|
||||
} catch (e: Exception) {
|
||||
Log.e("NIP44", "Unable to parse base64 $payload")
|
||||
return null
|
||||
}
|
||||
val byteArray = Base64.getDecoder().decode(ciphertext)
|
||||
|
||||
return when (byteArray[0].toInt()) {
|
||||
EncryptedInfo.V -> Nip04.decrypt(payload, privateKey, pubKey)
|
||||
Nip44v1.EncryptedInfo.V -> v1.decrypt(payload, privateKey, pubKey)
|
||||
Nip44v2.EncryptedInfo.V -> v2.decrypt(payload, privateKey, pubKey)
|
||||
else -> null
|
||||
EncryptedInfo.V -> Nip04.decrypt(ciphertext, privateKey, pubKey)
|
||||
Nip44v1.EncryptedInfo.V -> v1.decrypt(ciphertext, privateKey, pubKey)
|
||||
Nip44v2.EncryptedInfo.V -> v2.decrypt(ciphertext, privateKey, pubKey)
|
||||
else -> throw IllegalArgumentException("Invalid or unsupported NIP-44 version code ${byteArray[0].toInt()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip44Encryption
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.utils.LibSodiumInstance
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.Secp256k1Instance
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.util.Base64
|
||||
|
||||
class Nip44v1 {
|
||||
@@ -57,7 +57,7 @@ class Nip44v1 {
|
||||
)
|
||||
|
||||
return EncryptedInfo(
|
||||
ciphertext = cipher ?: ByteArray(0),
|
||||
ciphertext = cipher,
|
||||
nonce = nonce,
|
||||
)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class Nip44v1 {
|
||||
payload: String,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? {
|
||||
): String {
|
||||
val sharedSecret = getSharedSecret(privateKey, pubKey)
|
||||
return decrypt(payload, sharedSecret)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class Nip44v1 {
|
||||
encryptedInfo: EncryptedInfo,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? {
|
||||
): String {
|
||||
val sharedSecret = getSharedSecret(privateKey, pubKey)
|
||||
return decrypt(encryptedInfo, sharedSecret)
|
||||
}
|
||||
@@ -83,21 +83,21 @@ class Nip44v1 {
|
||||
fun decrypt(
|
||||
payload: String,
|
||||
sharedSecret: ByteArray,
|
||||
): String? {
|
||||
val encryptedInfo = EncryptedInfo.decodePayload(payload) ?: return null
|
||||
): String {
|
||||
val encryptedInfo = EncryptedInfo.decodePayload(payload)
|
||||
return decrypt(encryptedInfo, sharedSecret)
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
encryptedInfo: EncryptedInfo,
|
||||
sharedSecret: ByteArray,
|
||||
): String? =
|
||||
): String =
|
||||
LibSodiumInstance
|
||||
.cryptoStreamXChaCha20Xor(
|
||||
messageBytes = encryptedInfo.ciphertext,
|
||||
nonce = encryptedInfo.nonce,
|
||||
key = sharedSecret,
|
||||
)?.decodeToString()
|
||||
).decodeToString()
|
||||
|
||||
fun getSharedSecret(
|
||||
privateKey: ByteArray,
|
||||
@@ -127,19 +127,18 @@ class Nip44v1 {
|
||||
companion object {
|
||||
const val V: Int = 1
|
||||
|
||||
fun decodePayload(payload: String): EncryptedInfo? {
|
||||
return try {
|
||||
fun decodePayload(payload: String): EncryptedInfo =
|
||||
try {
|
||||
val byteArray = Base64.getDecoder().decode(payload)
|
||||
check(byteArray[0].toInt() == V)
|
||||
return EncryptedInfo(
|
||||
EncryptedInfo(
|
||||
nonce = byteArray.copyOfRange(1, 25),
|
||||
ciphertext = byteArray.copyOfRange(25, byteArray.size),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP44v1", "Unable to Parse encrypted payload: $payload")
|
||||
null
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("NIP-44v1 Unable to Parse encrypted payload: $payload", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun encodePayload(): String =
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip44Encryption
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf
|
||||
import com.vitorpamplona.quartz.utils.LibSodiumInstance
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.Secp256k1Instance
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.Base64
|
||||
@@ -88,26 +88,26 @@ class Nip44v2 {
|
||||
payload: String,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? = decrypt(payload, getConversationKey(privateKey, pubKey))
|
||||
): String = decrypt(payload, getConversationKey(privateKey, pubKey))
|
||||
|
||||
fun decrypt(
|
||||
decoded: EncryptedInfo,
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
): String? = decrypt(decoded, getConversationKey(privateKey, pubKey))
|
||||
): String = decrypt(decoded, getConversationKey(privateKey, pubKey))
|
||||
|
||||
fun decrypt(
|
||||
payload: String,
|
||||
conversationKey: ByteArray,
|
||||
): String? {
|
||||
val decoded = EncryptedInfo.decodePayload(payload) ?: return null
|
||||
): String {
|
||||
val decoded = EncryptedInfo.decodePayload(payload)
|
||||
return decrypt(decoded, conversationKey)
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
decoded: EncryptedInfo,
|
||||
conversationKey: ByteArray,
|
||||
): String? {
|
||||
): String {
|
||||
val messageKey = getMessageKeys(conversationKey, decoded.nonce)
|
||||
val calculatedMac = hmacAad(messageKey.hmacKey, decoded.ciphertext, decoded.nonce)
|
||||
|
||||
@@ -236,7 +236,7 @@ class Nip44v2 {
|
||||
companion object {
|
||||
const val V: Int = 2
|
||||
|
||||
fun decodePayload(payload: String): EncryptedInfo? {
|
||||
fun decodePayload(payload: String): EncryptedInfo {
|
||||
check(payload.length >= 132 || payload.length <= 87472) {
|
||||
"Invalid payload length ${payload.length} for $payload"
|
||||
}
|
||||
@@ -245,14 +245,14 @@ class Nip44v2 {
|
||||
return try {
|
||||
val byteArray = Base64.getDecoder().decode(payload)
|
||||
check(byteArray[0].toInt() == V)
|
||||
return EncryptedInfo(
|
||||
EncryptedInfo(
|
||||
nonce = byteArray.copyOfRange(1, 33),
|
||||
ciphertext = byteArray.copyOfRange(33, byteArray.size - 32),
|
||||
mac = byteArray.copyOfRange(byteArray.size - 32, byteArray.size),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("NIP44v2", "Unable to Parse encrypted payload: $payload")
|
||||
null
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("NIP-44v2 Unable to Parse encrypted payload: $payload", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-43
@@ -25,10 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class NostrConnectEvent(
|
||||
@@ -39,17 +39,18 @@ class NostrConnectEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var decryptedContent: Map<HexKey, BunkerMessage> = mapOf()
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (decryptedContent.values.sumOf { pointerSizeInBytes + it.countMemory() })
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || recipientPubKey() == signer.pubKey
|
||||
|
||||
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull()
|
||||
suspend fun decryptMessage(signer: NostrSigner): BunkerMessage {
|
||||
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
val retVal = signer.decrypt(content, talkingWith(signer.pubKey))
|
||||
return JsonMapper.mapper.readValue(retVal, BunkerMessage::class.java)
|
||||
}
|
||||
|
||||
private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
|
||||
fun verifiedRecipientPubKey(): HexKey? {
|
||||
val recipient = recipientPubKey()
|
||||
@@ -62,47 +63,29 @@ class NostrConnectEvent(
|
||||
|
||||
fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey
|
||||
|
||||
fun plainContent(
|
||||
signer: NostrSigner,
|
||||
onReady: (BunkerMessage) -> Unit,
|
||||
) {
|
||||
decryptedContent[signer.pubKey]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
// decrypts using NIP-04 or NIP-44
|
||||
signer.decrypt(content, talkingWith(signer.pubKey)) { retVal ->
|
||||
val content = JsonMapper.mapper.readValue(retVal, BunkerMessage::class.java)
|
||||
|
||||
decryptedContent = decryptedContent + Pair(signer.pubKey, content)
|
||||
|
||||
onReady(content)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 24133
|
||||
const val ALT = "Nostr Connect Event"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
message: BunkerMessage,
|
||||
remoteKey: HexKey,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (NostrConnectEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
arrayOf(
|
||||
AltTag.assemble(ALT),
|
||||
arrayOf("p", remoteKey),
|
||||
)
|
||||
|
||||
val encrypted = JsonMapper.mapper.writeValueAsString(message)
|
||||
|
||||
signer.nip44Encrypt(encrypted, remoteKey) { content ->
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
): NostrConnectEvent =
|
||||
signer.sign(
|
||||
createdAt = createdAt,
|
||||
kind = KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
AltTag.assemble(ALT),
|
||||
arrayOf("p", remoteKey),
|
||||
),
|
||||
content =
|
||||
signer.nip44Encrypt(
|
||||
plaintext = JsonMapper.mapper.writeValueAsString(message),
|
||||
toPublicKey = remoteKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-37
@@ -20,16 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class LnZapPaymentRequestEvent(
|
||||
@@ -40,60 +38,41 @@ class LnZapPaymentRequestEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
// Once one of an app user decrypts the payment, all users else can see it.
|
||||
@Transient private var lnInvoice: String? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (lnInvoice?.bytesUsedInMemory() ?: 0) // rough calculation
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun walletServicePubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
|
||||
fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) walletServicePubKey() ?: pubKey else pubKey
|
||||
|
||||
fun lnInvoice(
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
lnInvoice?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || walletServicePubKey() == signer.pubKey
|
||||
|
||||
try {
|
||||
signer.decrypt(content, talkingWith(signer.pubKey)) { jsonText ->
|
||||
val payInvoiceMethod = JsonMapper.mapper.readValue(jsonText, Request::class.java)
|
||||
|
||||
lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice
|
||||
|
||||
lnInvoice?.let { onReady(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
|
||||
}
|
||||
suspend fun decryptRequest(signer: NostrSigner): Request {
|
||||
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val jsonText = signer.decrypt(content, talkingWith(signer.pubKey))
|
||||
return JsonMapper.mapper.readValue(jsonText, Request::class.java)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 23194
|
||||
const val ALT = "Zap payment request"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
lnInvoice: String,
|
||||
walletServicePubkey: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (LnZapPaymentRequestEvent) -> Unit,
|
||||
) {
|
||||
): LnZapPaymentRequestEvent {
|
||||
val serializedRequest = JsonMapper.mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice))
|
||||
|
||||
val tags = arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT))
|
||||
|
||||
signer.nip04Encrypt(
|
||||
serializedRequest,
|
||||
walletServicePubkey,
|
||||
) { content ->
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
val encrypted =
|
||||
signer.nip04Encrypt(
|
||||
serializedRequest,
|
||||
walletServicePubkey,
|
||||
)
|
||||
|
||||
return signer.sign(createdAt, KIND, tags, encrypted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-36
@@ -20,13 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
|
||||
@Immutable
|
||||
class LnZapPaymentResponseEvent(
|
||||
@@ -37,10 +36,7 @@ class LnZapPaymentResponseEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
// Once one of an app user decrypts the payment, all users else can see it.
|
||||
@Transient private var response: Response? = null
|
||||
|
||||
override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (response?.countMemory() ?: 0)
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun requestAuthor() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
|
||||
@@ -48,38 +44,13 @@ class LnZapPaymentResponseEvent(
|
||||
|
||||
fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) requestAuthor() ?: pubKey else pubKey
|
||||
|
||||
private fun plainContent(
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
try {
|
||||
signer.decrypt(content, talkingWith(signer.pubKey)) { content -> onReady(content) }
|
||||
} catch (e: Exception) {
|
||||
Log.w("PrivateDM", "Error decrypting the message ${e.message}")
|
||||
}
|
||||
}
|
||||
fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || requestAuthor() == signer.pubKey
|
||||
|
||||
fun response(
|
||||
signer: NostrSigner,
|
||||
onReady: (Response) -> Unit,
|
||||
) {
|
||||
response?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
suspend fun decrypt(signer: NostrSigner): Response {
|
||||
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
try {
|
||||
if (content.isNotEmpty()) {
|
||||
plainContent(signer) {
|
||||
JsonMapper.mapper.readValue(it, Response::class.java)?.let {
|
||||
response = it
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e)
|
||||
}
|
||||
val json = signer.decrypt(content, talkingWith(signer.pubKey))
|
||||
return JsonMapper.mapper.readValue(json, Response::class.java)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ class Nip47WalletConnect {
|
||||
val relayUri: String,
|
||||
val secret: HexKey?,
|
||||
) {
|
||||
fun normalize(): Nip47WalletConnect.Nip47URINorm? =
|
||||
fun normalize(): Nip47URINorm? =
|
||||
RelayUrlNormalizer.normalizeOrNull(relayUri)?.let {
|
||||
Nip47URINorm(
|
||||
pubKeyHex,
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.nip47WalletConnect
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
|
||||
class NostrWalletConnectRequestCache(
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
private val decryptionCache =
|
||||
object : LruCache<LnZapPaymentRequestEvent, NWCRequestDecryptCache>(50) {
|
||||
override fun create(key: LnZapPaymentRequestEvent): NWCRequestDecryptCache? =
|
||||
if (key.content.isNotBlank() && key.canDecrypt(signer)) {
|
||||
NWCRequestDecryptCache(signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun cachedRequest(event: LnZapPaymentRequestEvent): Request? = decryptionCache[event]?.cached()
|
||||
|
||||
suspend fun decryptRequest(event: LnZapPaymentRequestEvent) = decryptionCache[event]?.decrypt(event)
|
||||
}
|
||||
|
||||
class NWCRequestDecryptCache(
|
||||
signer: NostrSigner,
|
||||
) : DecryptCache<LnZapPaymentRequestEvent, Request>(signer) {
|
||||
override suspend fun decryptAndParse(
|
||||
event: LnZapPaymentRequestEvent,
|
||||
signer: NostrSigner,
|
||||
) = event.decryptRequest(signer)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.nip47WalletConnect
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
|
||||
class NostrWalletConnectResponseCache(
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
private val decryptionCache =
|
||||
object : LruCache<LnZapPaymentResponseEvent, NWCResponseDecryptCache>(50) {
|
||||
override fun create(key: LnZapPaymentResponseEvent): NWCResponseDecryptCache? =
|
||||
if (key.content.isNotBlank() && key.canDecrypt(signer)) {
|
||||
NWCResponseDecryptCache(signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun cachedResponse(event: LnZapPaymentResponseEvent): Response? = decryptionCache[event]?.cached()
|
||||
|
||||
suspend fun decryptResponse(event: LnZapPaymentResponseEvent) = decryptionCache[event]?.decrypt(event)
|
||||
}
|
||||
|
||||
class NWCResponseDecryptCache(
|
||||
signer: NostrSigner,
|
||||
) : DecryptCache<LnZapPaymentResponseEvent, Response>(signer) {
|
||||
override suspend fun decryptAndParse(
|
||||
event: LnZapPaymentResponseEvent,
|
||||
signer: NostrSigner,
|
||||
) = event.decrypt(signer)
|
||||
}
|
||||
@@ -21,16 +21,27 @@
|
||||
package com.vitorpamplona.quartz.nip50Search
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip50Search.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.searchRelays
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.toTypedArray
|
||||
|
||||
@Immutable
|
||||
class SearchRelayListEvent(
|
||||
@@ -40,66 +51,72 @@ class SearchRelayListEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun relays(): List<NormalizedRelayUrl> = tags.mapNotNull(RelayTag::parse)
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun publicRelays() = tags.relays()
|
||||
|
||||
suspend fun privateRelays(signer: NostrSigner) = privateTags(signer)?.relays()
|
||||
|
||||
suspend fun relays(signer: NostrSigner): List<NormalizedRelayUrl> = publicRelays() + (privateRelays(signer) ?: emptyList())
|
||||
|
||||
companion object {
|
||||
const val KIND = 10007
|
||||
val ALT = "Relay list to use for Search"
|
||||
val ALT_TAG = arrayOf(AltTag.assemble(ALT))
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey)
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.plusElement(AltTag.assemble("Relay list to use for Search"))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
suspend fun updateRelayList(
|
||||
earlierVersion: SearchRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (SearchRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter(RelayTag::notMatch)
|
||||
.plus(
|
||||
relays.map {
|
||||
RelayTag.assemble(it)
|
||||
},
|
||||
).toTypedArray()
|
||||
): SearchRelayListEvent {
|
||||
val newRelayList = relays.map { RelayTag.assemble(it) }
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
val publicTags = earlierVersion.tags.remove(RelayTag::match)
|
||||
val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList)
|
||||
|
||||
return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
suspend fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (SearchRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (SearchRelayListEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
): SearchRelayListEvent {
|
||||
val publicTagArray = relays.map { RelayTag.assemble(it) }.plus(ALT_TAG).toTypedArray()
|
||||
return signer.signNip51List(createdAt, KIND, publicTagArray, emptyArray())
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): SearchRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
): SearchRelayListEvent {
|
||||
val publicTagArray = relays.map { RelayTag.assemble(it) }.plus(ALT_TAG).toTypedArray()
|
||||
return signer.signNip51List(createdAt, KIND, publicTagArray, emptyArray())
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicRelays: List<NormalizedRelayUrl> = emptyList(),
|
||||
privateRelays: List<NormalizedRelayUrl> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<SearchRelayListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<SearchRelayListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
searchRelays(publicRelays)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip50Search.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class BlockedRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun relays(): List<NormalizedRelayUrl> = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10006
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.plusElement(AltTag.assemble("Relay list to use for trusted connections"))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: BlockedRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter(RelayTag::notMatch)
|
||||
.plus(
|
||||
relays.map {
|
||||
RelayTag.assemble(it)
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BlockedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
@@ -1,237 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class BookmarkListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun countBookmarks() = tags.count(ETag::isTagged) + tags.count(ATag::isTagged)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30001
|
||||
const val ALT = "List of bookmarks"
|
||||
const val DEFAULT_D_TAG_BOOKMARKS = "bookmark"
|
||||
|
||||
fun addEvent(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
eventId: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addReplaceable(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
aTag: ATag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addTag(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
tagName: String,
|
||||
tagValue: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) {
|
||||
add(
|
||||
earlierVersion,
|
||||
arrayOf(arrayOf(tagName, tagValue)),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun add(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
listNewTags: Array<Array<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) {
|
||||
if (isPrivate) {
|
||||
if (earlierVersion != null) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.plus(listNewTags),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
encryptTags(
|
||||
privateTags = listNewTags,
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = arrayOf(arrayOf("d", DEFAULT_D_TAG_BOOKMARKS)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion?.content ?: "",
|
||||
tags = (earlierVersion?.tags ?: arrayOf(arrayOf("d", DEFAULT_D_TAG_BOOKMARKS))).plus(listNewTags),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEvent(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
eventId: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun removeReplaceable(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
aTag: ATag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady)
|
||||
|
||||
private fun removeTag(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
tagName: String,
|
||||
tagValue: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) {
|
||||
if (isPrivate) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags =
|
||||
privateTags
|
||||
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
name: String = "",
|
||||
events: List<String>? = null,
|
||||
users: List<String>? = null,
|
||||
addresses: List<ATag>? = null,
|
||||
privEvents: List<String>? = null,
|
||||
privUsers: List<String>? = null,
|
||||
privAddresses: List<ATag>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
tags.add(arrayOf("d", name))
|
||||
|
||||
events?.forEach { tags.add(arrayOf("e", it)) }
|
||||
users?.forEach { tags.add(arrayOf("p", it)) }
|
||||
addresses?.forEach { tags.add(arrayOf("a", it.toTag())) }
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
createPrivateTags(privEvents, privUsers, privAddresses, signer) { content ->
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isTagged
|
||||
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.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
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.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Immutable
|
||||
abstract class GeneralListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ETag::parseAsHint) ?: emptyList())
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + (cachedPrivateTags()?.mapNotNull(ETag::parseId) ?: emptyList())
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ATag::parseAsHint) ?: emptyList())
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + (cachedPrivateTags()?.mapNotNull(ATag::parseAddressId) ?: emptyList())
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(PTag::parseAsHint) ?: emptyList())
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + (cachedPrivateTags()?.mapNotNull(PTag::parseKey) ?: emptyList())
|
||||
|
||||
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
|
||||
|
||||
@Deprecated("NIP-51 has deprecated Title. Use name instead", ReplaceWith("name()"))
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
fun nameOrTitle() = name() ?: title()
|
||||
|
||||
fun filterTagList(
|
||||
key: String,
|
||||
privateTags: Array<Array<String>>?,
|
||||
): ImmutableSet<String> {
|
||||
val result = HashSet<String>(tags.size + (privateTags?.size ?: 0))
|
||||
|
||||
privateTags?.let { it.filter { it.size > 1 && it[0] == key }.mapTo(result) { it[1] } }
|
||||
|
||||
tags.filter { it.size > 1 && it[0] == key }.mapTo(result) { it[1] }
|
||||
|
||||
return result.toImmutableSet()
|
||||
}
|
||||
|
||||
fun isTagged(
|
||||
key: String,
|
||||
tag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (Boolean) -> Unit,
|
||||
) = if (isPrivate) {
|
||||
privateTagsOrEmpty(signer = signer) {
|
||||
onReady(
|
||||
it.any { it.size > 1 && it[0] == key && it[1] == tag },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
onReady(tags.isTagged(key, tag))
|
||||
}
|
||||
|
||||
fun privateTagsOrEmpty(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
privateTags(signer, onReady)
|
||||
}
|
||||
|
||||
fun privateTaggedUsers(
|
||||
signer: NostrSigner,
|
||||
onReady: (List<String>) -> Unit,
|
||||
) = privateTags(signer) { onReady(filterUsers(it)) }
|
||||
|
||||
fun privateHashtags(
|
||||
signer: NostrSigner,
|
||||
onReady: (List<String>) -> Unit,
|
||||
) = privateTags(signer) { onReady(filterHashtags(it)) }
|
||||
|
||||
fun privateGeohashes(
|
||||
signer: NostrSigner,
|
||||
onReady: (List<String>) -> Unit,
|
||||
) = privateTags(signer) { onReady(filterGeohashes(it)) }
|
||||
|
||||
fun privateTaggedEvents(
|
||||
signer: NostrSigner,
|
||||
onReady: (List<String>) -> Unit,
|
||||
) = privateTags(signer) { onReady(filterEvents(it)) }
|
||||
|
||||
fun privateATags(
|
||||
signer: NostrSigner,
|
||||
onReady: (List<ATag>) -> Unit,
|
||||
) = privateTags(signer) { onReady(filterATags(it)) }
|
||||
|
||||
fun privateAddress(
|
||||
signer: NostrSigner,
|
||||
onReady: (List<Address>) -> Unit,
|
||||
) = privateTags(signer) { onReady(filterAddresses(it)) }
|
||||
|
||||
fun filterUsers(tags: Array<Array<String>>): List<String> = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
fun filterHashtags(tags: Array<Array<String>>): List<String> = tags.mapNotNull(HashtagTag::parse)
|
||||
|
||||
fun filterGeohashes(tags: Array<Array<String>>): List<String> = tags.geohashes()
|
||||
|
||||
fun filterEvents(tags: Array<Array<String>>): List<String> = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
fun filterATags(tags: Array<Array<String>>): List<ATag> = tags.mapNotNull(ATag::parse)
|
||||
|
||||
fun filterAddresses(tags: Array<Array<String>>): List<Address> = tags.mapNotNull(ATag::parseAddress)
|
||||
|
||||
companion object {
|
||||
fun createPrivateTags(
|
||||
privEvents: List<String>? = null,
|
||||
privUsers: List<String>? = null,
|
||||
privAddresses: List<ATag>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val privTags = mutableListOf<Array<String>>()
|
||||
privEvents?.forEach { privTags.add(arrayOf("e", it)) }
|
||||
privUsers?.forEach { privTags.add(arrayOf("p", it)) }
|
||||
privAddresses?.forEach { privTags.add(arrayOf("a", it.toTag())) }
|
||||
|
||||
return encryptTags(privTags.toTypedArray(), signer, onReady)
|
||||
}
|
||||
|
||||
fun encryptTags(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val msg = JsonMapper.mapper.writeValueAsString(privateTags)
|
||||
|
||||
signer.nip04Encrypt(
|
||||
msg,
|
||||
signer.pubKey,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,285 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent.UsersAndWords
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Immutable
|
||||
class MuteListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun publicAndCachedUsersAndWords() =
|
||||
UsersAndWords(
|
||||
filterTagList("p", cachedPrivateTags()),
|
||||
filterTagList("word", cachedPrivateTags()),
|
||||
)
|
||||
|
||||
fun publicAndPrivateUsersAndWords(
|
||||
signer: NostrSigner,
|
||||
onReady: (UsersAndWords) -> Unit,
|
||||
) {
|
||||
privateTagsOrEmpty(signer) {
|
||||
onReady(
|
||||
UsersAndWords(
|
||||
filterTagList("p", it),
|
||||
filterTagList("word", it),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? =
|
||||
tryAndWait { continuation ->
|
||||
publicAndPrivateUsersAndWords(signer) { privateTagList ->
|
||||
continuation.resume(privateTagList)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10000
|
||||
const val FIXED_D_TAG = ""
|
||||
const val ALT = "Mute List"
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun blockListFor(pubKeyHex: HexKey): String = "10000:$pubKeyHex:"
|
||||
|
||||
fun createListWithTag(
|
||||
key: String,
|
||||
tag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
if (isPrivate) {
|
||||
encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = emptyArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = "",
|
||||
tags = arrayOf(arrayOf(key, tag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createListWithUser(
|
||||
pubKeyHex: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) = createListWithTag("p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun createListWithWord(
|
||||
word: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) = createListWithTag("word", word, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addUsers(
|
||||
earlierVersion: MuteListEvent,
|
||||
listPubKeyHex: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
if (isPrivate) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags =
|
||||
privateTags.plus(
|
||||
listPubKeyHex.map { arrayOf("p", it) },
|
||||
),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags.plus(
|
||||
listPubKeyHex.map { arrayOf("p", it) },
|
||||
),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addWord(
|
||||
earlierVersion: MuteListEvent,
|
||||
word: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addUser(
|
||||
earlierVersion: MuteListEvent,
|
||||
pubKeyHex: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addTag(
|
||||
earlierVersion: MuteListEvent,
|
||||
key: String,
|
||||
tag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged ->
|
||||
if (!isTagged) {
|
||||
if (isPrivate) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.plus(element = arrayOf(key, tag)),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = arrayOf(key, tag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeWord(
|
||||
earlierVersion: MuteListEvent,
|
||||
word: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "word", word, signer, createdAt, onReady)
|
||||
|
||||
fun removeUser(
|
||||
earlierVersion: MuteListEvent,
|
||||
pubKeyHex: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "p", pubKeyHex, signer, createdAt, onReady)
|
||||
|
||||
fun removeTag(
|
||||
earlierVersion: MuteListEvent,
|
||||
key: String,
|
||||
tag: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags =
|
||||
privateTags
|
||||
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,305 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Immutable
|
||||
class PeopleListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Immutable
|
||||
class UsersAndWords(
|
||||
val users: Set<String> = setOf(),
|
||||
val words: Set<String> = setOf(),
|
||||
)
|
||||
|
||||
fun publicAndCachedPrivateUsersAndWords() =
|
||||
UsersAndWords(
|
||||
filterTagList("p", cachedPrivateTags()),
|
||||
filterTagList("word", cachedPrivateTags()),
|
||||
)
|
||||
|
||||
fun publicAndPrivateUsersAndWords(
|
||||
signer: NostrSigner,
|
||||
onReady: (UsersAndWords) -> Unit,
|
||||
) {
|
||||
privateTagsOrEmpty(signer) {
|
||||
onReady(
|
||||
UsersAndWords(
|
||||
filterTagList("p", it),
|
||||
filterTagList("word", it),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? =
|
||||
tryAndWait { continuation ->
|
||||
publicAndPrivateUsersAndWords(signer) { privateTagList ->
|
||||
continuation.resume(privateTagList)
|
||||
}
|
||||
}
|
||||
|
||||
fun isTaggedWord(
|
||||
word: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (Boolean) -> Unit,
|
||||
) = isTagged("word", word, isPrivate, signer, onReady)
|
||||
|
||||
fun isTaggedUser(
|
||||
idHex: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (Boolean) -> Unit,
|
||||
) = isTagged("p", idHex, isPrivate, signer, onReady)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30000
|
||||
const val BLOCK_LIST_D_TAG = "mute"
|
||||
const val ALT = "List of people"
|
||||
|
||||
fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG)
|
||||
|
||||
fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
|
||||
|
||||
fun createListWithTag(
|
||||
name: String,
|
||||
key: String,
|
||||
tag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
if (isPrivate) {
|
||||
encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = arrayOf(arrayOf("d", name)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = "",
|
||||
tags = arrayOf(arrayOf("d", name), arrayOf(key, tag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createListWithUser(
|
||||
name: String,
|
||||
pubKeyHex: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) = createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun createListWithWord(
|
||||
name: String,
|
||||
word: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) = createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addUsers(
|
||||
earlierVersion: PeopleListEvent,
|
||||
listPubKeyHex: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
if (isPrivate) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags =
|
||||
privateTags.plus(
|
||||
listPubKeyHex.map { arrayOf("p", it) },
|
||||
),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags.plus(
|
||||
listPubKeyHex.map { arrayOf("p", it) },
|
||||
),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addWord(
|
||||
earlierVersion: PeopleListEvent,
|
||||
word: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addUser(
|
||||
earlierVersion: PeopleListEvent,
|
||||
pubKeyHex: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addTag(
|
||||
earlierVersion: PeopleListEvent,
|
||||
key: String,
|
||||
tag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged ->
|
||||
if (!isTagged) {
|
||||
if (isPrivate) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.plus(element = arrayOf(key, tag)),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = arrayOf(key, tag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeWord(
|
||||
earlierVersion: PeopleListEvent,
|
||||
word: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "word", word, signer, createdAt, onReady)
|
||||
|
||||
fun removeUser(
|
||||
earlierVersion: PeopleListEvent,
|
||||
pubKeyHex: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "p", pubKeyHex, signer, createdAt, onReady)
|
||||
|
||||
fun removeTag(
|
||||
earlierVersion: PeopleListEvent,
|
||||
key: String,
|
||||
tag: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags =
|
||||
privateTags
|
||||
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,17 +42,16 @@ class PinListEvent(
|
||||
const val KIND = 33888
|
||||
const val ALT = "Pinned Posts"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
pins: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PinListEvent) -> Unit,
|
||||
) {
|
||||
): PinListEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
pins.forEach { tags.add(arrayOf("pin", it)) }
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady)
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-90
@@ -21,194 +21,170 @@
|
||||
package com.vitorpamplona.quartz.nip51Lists
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
|
||||
class PrivateTagArrayBuilder {
|
||||
companion object {
|
||||
fun create(
|
||||
suspend fun create(
|
||||
tags: Array<Array<String>>,
|
||||
toPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
): Pair<String, Array<Array<String>>> =
|
||||
if (toPrivate) {
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = tags,
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, arrayOf())
|
||||
}
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = tags,
|
||||
signer = signer,
|
||||
)
|
||||
Pair(encryptedTags, arrayOf())
|
||||
} else {
|
||||
onReady("", tags)
|
||||
Pair("", tags)
|
||||
}
|
||||
}
|
||||
|
||||
fun add(
|
||||
suspend fun add(
|
||||
current: PrivateTagArrayEvent,
|
||||
newTag: Array<String>,
|
||||
toPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
): Pair<String, Array<Array<String>>> =
|
||||
if (toPrivate) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = privateTags.plus(newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags)
|
||||
}
|
||||
}
|
||||
)
|
||||
Pair(encryptedTags, current.tags)
|
||||
} else {
|
||||
onReady(current.content, current.tags.plus(newTag))
|
||||
Pair(current.content, current.tags.plus(newTag))
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(
|
||||
suspend fun addAll(
|
||||
current: PrivateTagArrayEvent,
|
||||
newTag: Array<Array<String>>,
|
||||
toPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
): Pair<String, Array<Array<String>>> =
|
||||
if (toPrivate) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = privateTags.plus(newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags)
|
||||
}
|
||||
}
|
||||
)
|
||||
Pair(encryptedTags, current.tags)
|
||||
} else {
|
||||
onReady(current.content, current.tags.plus(newTag))
|
||||
Pair(current.content, current.tags.plus(newTag))
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPrivateNewTag(
|
||||
suspend fun replaceAllToPrivateNewTag(
|
||||
dTag: String,
|
||||
current: PrivateTagArrayEvent?,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
): Pair<String, Array<Array<String>>> =
|
||||
if (current == null) {
|
||||
createPrivate(dTag, newTag, signer, onReady)
|
||||
createPrivate(dTag, newTag, signer)
|
||||
} else {
|
||||
replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady)
|
||||
replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPublicNewTag(
|
||||
suspend fun replaceAllToPublicNewTag(
|
||||
dTag: String,
|
||||
current: PrivateTagArrayEvent?,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
): Pair<String, Array<Array<String>>> =
|
||||
if (current == null) {
|
||||
createPublic(dTag, newTag, signer, onReady)
|
||||
createPublic(dTag, newTag, signer)
|
||||
} else {
|
||||
replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady)
|
||||
replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPrivateNewTag(
|
||||
suspend fun replaceAllToPrivateNewTag(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
): Pair<String, Array<Array<String>>> {
|
||||
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = privateTags.replaceAll(oldTagStartsWith, newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
|
||||
}
|
||||
}
|
||||
)
|
||||
return Pair(encryptedTags, current.tags.remove(oldTagStartsWith))
|
||||
}
|
||||
|
||||
fun replaceAllToPublicNewTag(
|
||||
suspend fun replaceAllToPublicNewTag(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
): Pair<String, Array<Array<String>>> {
|
||||
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = privateTags.remove(oldTagStartsWith),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag))
|
||||
}
|
||||
}
|
||||
)
|
||||
return Pair(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag))
|
||||
}
|
||||
|
||||
fun removeAllFromPrivate(
|
||||
suspend fun removeAllFromPrivate(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
): Pair<String, Array<Array<String>>> {
|
||||
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = privateTags.remove(oldTagStartsWith),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags)
|
||||
}
|
||||
}
|
||||
)
|
||||
return Pair(encryptedTags, current.tags)
|
||||
}
|
||||
|
||||
fun removeAllFromPublic(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) = onReady(current.content, current.tags.remove(oldTagStartsWith))
|
||||
): Pair<String, Array<Array<String>>> = Pair(current.content, current.tags.remove(oldTagStartsWith))
|
||||
|
||||
fun removeAll(
|
||||
suspend fun removeAll(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
): Pair<String, Array<Array<String>>> {
|
||||
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = privateTags.remove(oldTagStartsWith),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
|
||||
}
|
||||
}
|
||||
)
|
||||
return Pair(encryptedTags, current.tags.remove(oldTagStartsWith))
|
||||
}
|
||||
|
||||
fun createPrivate(
|
||||
suspend fun createPrivate(
|
||||
dTag: String,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = arrayOf(newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, arrayOf(arrayOf("d", dTag)))
|
||||
}
|
||||
): Pair<String, Array<Array<String>>> {
|
||||
val encryptedTags =
|
||||
PrivateTagsInContent.encryptNip04(
|
||||
privateTags = arrayOf(newTag),
|
||||
signer = signer,
|
||||
)
|
||||
return Pair(encryptedTags, arrayOf(arrayOf("d", dTag)))
|
||||
}
|
||||
|
||||
fun createPublic(
|
||||
dTag: String,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
onReady("", arrayOf(arrayOf("d", dTag), newTag))
|
||||
}
|
||||
): Pair<String, Array<Array<String>>> = Pair("", arrayOf(arrayOf("d", dTag), newTag))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip51Lists
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Immutable
|
||||
abstract class PrivateTagArrayEvent(
|
||||
@@ -35,61 +35,28 @@ abstract class PrivateTagArrayEvent(
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
tags: TagArray,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun cachedPrivateTags(): Array<Array<String>>? = privateTagsCache
|
||||
suspend fun decrypt(signer: NostrSigner): TagArray {
|
||||
if (signer.pubKey != pubKey) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
fun privateTags(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) {
|
||||
onReady(emptyArray())
|
||||
return
|
||||
}
|
||||
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
PrivateTagsInContent.decrypt(content, signer) {
|
||||
privateTagsCache = it
|
||||
privateTagsCache?.let { onReady(it) }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
onReady(emptyArray())
|
||||
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||
}
|
||||
return PrivateTagsInContent.decrypt(content, signer)
|
||||
}
|
||||
|
||||
fun mergeTagList(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
privateTags(signer) {
|
||||
onReady(tags + it)
|
||||
suspend fun privateTags(signer: NostrSigner): TagArray? {
|
||||
if (signer.pubKey != pubKey) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> mapAllTags(
|
||||
privateTags: Array<Array<String>>,
|
||||
mapper: (Array<String>) -> T,
|
||||
): Set<T> {
|
||||
val privateRooms = privateTags.mapNotNull(mapper)
|
||||
val publicRooms = tags.mapNotNull(mapper)
|
||||
|
||||
return (privateRooms + publicRooms).toSet()
|
||||
return try {
|
||||
PrivateTagsInContent.decrypt(content, signer)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
import kotlin.collections.plus
|
||||
|
||||
class PrivateTagArrayEventCache<T : PrivateTagArrayEvent>(
|
||||
signer: NostrSigner,
|
||||
cacheSize: Int = 10,
|
||||
) {
|
||||
private val decryptionCache =
|
||||
object : LruCache<T, PrivateTagArrayEventDecryptCache<T>>(cacheSize) {
|
||||
override fun create(key: T): PrivateTagArrayEventDecryptCache<T>? =
|
||||
if (key.content.isNotBlank() && key.pubKey == signer.pubKey) {
|
||||
PrivateTagArrayEventDecryptCache<T>(signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(event: T) = decryptionCache.remove(event)
|
||||
|
||||
fun cachedPrivateTags(event: T): TagArray? = decryptionCache[event]?.cached()
|
||||
|
||||
suspend fun privateTags(event: T) = decryptionCache[event]?.decrypt(event)
|
||||
|
||||
suspend fun mergeTagList(event: T): TagArray = event.tags + (privateTags(event) ?: emptyArray())
|
||||
|
||||
fun mergeTagListPrecached(event: T): TagArray = event.tags + (cachedPrivateTags(event) ?: emptyArray())
|
||||
}
|
||||
|
||||
class PrivateTagArrayEventDecryptCache<T : PrivateTagArrayEvent>(
|
||||
signer: NostrSigner,
|
||||
) : DecryptCache<T, TagArray>(signer) {
|
||||
override suspend fun decryptAndParse(
|
||||
event: T,
|
||||
signer: NostrSigner,
|
||||
): TagArray = event.decrypt(signer)
|
||||
}
|
||||
@@ -1,62 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class RelaySetEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun relays(): List<NormalizedRelayUrl> = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30002
|
||||
const val ALT = "Relay list"
|
||||
|
||||
fun create(
|
||||
relays: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (RelaySetEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
relays.forEach { tags.add(arrayOf("r", it)) }
|
||||
tags.add(AltTag.assemble(ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip51Lists
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.utils.startsWith
|
||||
import com.vitorpamplona.quartz.utils.startsWithAny
|
||||
|
||||
inline fun TagArray.filterToArray(predicate: (Array<String>) -> Boolean): TagArray = filterTo(ArrayList(), predicate).toTypedArray()
|
||||
|
||||
@@ -29,6 +30,14 @@ inline fun TagArray.remove(predicate: (Array<String>) -> Boolean): TagArray = fi
|
||||
|
||||
fun TagArray.remove(startsWith: Array<String>): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray()
|
||||
|
||||
fun TagArray.removeAny(startsWith: List<Array<String>>): TagArray =
|
||||
filterNotTo(
|
||||
ArrayList(this.size),
|
||||
{
|
||||
it.startsWithAny(startsWith)
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
fun TagArray.replaceAll(
|
||||
startsWith: Array<String>,
|
||||
newElement: Array<String>,
|
||||
|
||||
@@ -1,105 +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.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip50Search.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class TrustedRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun relays(): List<NormalizedRelayUrl> = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10089
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.plusElement(AltTag.assemble("Relay list to use for trusted connections"))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: TrustedRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter(RelayTag::notMatch)
|
||||
.plus(
|
||||
relays.map {
|
||||
RelayTag.assemble(it)
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): TrustedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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.bookmarkList
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.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.NameTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class BookmarkListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider {
|
||||
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
|
||||
|
||||
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
|
||||
|
||||
fun countBookmarks() = tags.count(BookmarkIdTag::isTagged)
|
||||
|
||||
fun publicBookmarks(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
|
||||
|
||||
suspend fun privateBookmarks(signer: NostrSigner): List<BookmarkIdTag>? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30001
|
||||
const val ALT = "List of bookmarks"
|
||||
const val DEFAULT_D_TAG_BOOKMARKS = "bookmark"
|
||||
|
||||
fun createBookmarkAddress(pubKey: HexKey) = Address(KIND, pubKey, DEFAULT_D_TAG_BOOKMARKS)
|
||||
|
||||
suspend fun create(
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BookmarkListEvent =
|
||||
if (isPrivate) {
|
||||
create(
|
||||
publicBookmarks = emptyList(),
|
||||
privateBookmarks = listOf(bookmarkIdTag),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
publicBookmarks = listOf(bookmarkIdTag),
|
||||
privateBookmarks = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BookmarkListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
tags = earlierVersion.tags,
|
||||
privateTags = privateTags.plus(bookmarkIdTag.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(bookmarkIdTag.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BookmarkListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
tags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
|
||||
tags = tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BookmarkListEvent {
|
||||
val newTags =
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
name: String = "",
|
||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
dTag: String = DEFAULT_D_TAG_BOOKMARKS,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BookmarkListEvent {
|
||||
val template = build(name, publicBookmarks, privateBookmarks, signer, dTag, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
name: String = "",
|
||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
dTag: String = DEFAULT_D_TAG_BOOKMARKS,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<BookmarkListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip04(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
dTag(dTag)
|
||||
alt(ALT)
|
||||
name(name)
|
||||
bookmarks(publicBookmarks)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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.bookmarkList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||
|
||||
fun TagArrayBuilder<BookmarkListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
||||
|
||||
fun TagArrayBuilder<BookmarkListEvent>.bookmarks(bookmarks: List<BookmarkIdTag>) = addAll(bookmarks.map { it.toTagArray() })
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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.bookmarkList.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
class AddressBookmark(
|
||||
val address: Address,
|
||||
val relayHint: NormalizedRelayUrl? = null,
|
||||
) : BookmarkIdTag {
|
||||
fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
|
||||
|
||||
override fun toTagArray() = assemble(address, relayHint)
|
||||
|
||||
override fun toTagIdOnly() = assemble(address, null)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "a"
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
addressId: String,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
address: AddressBookmark,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
|
||||
|
||||
@JvmStatic
|
||||
fun isIn(
|
||||
tag: Array<String>,
|
||||
addressIds: Set<String>,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
|
||||
|
||||
@JvmStatic
|
||||
fun isTaggedWithKind(
|
||||
tag: Array<String>,
|
||||
kind: String,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind)
|
||||
|
||||
@JvmStatic
|
||||
fun parse(
|
||||
aTagId: String,
|
||||
relay: String?,
|
||||
) = Address.parse(aTagId)?.let {
|
||||
AddressBookmark(it, relay?.let { RelayUrlNormalizer.normalizeOrNull(it) })
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): AddressBookmark? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return parse(tag[1], tag.getOrNull(2))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseValidAddress(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return Address.parse(tag[1])?.toValue()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseAddress(tag: Array<String>): Address? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return Address.parse(tag[1])
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseAddressId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[1].contains(':')) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
ensure(relayHint != null) { return null }
|
||||
|
||||
return AddressHint(tag[1], relayHint)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
aTagId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url)
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
address: Address,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url)
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
kind: Int,
|
||||
pubKey: String,
|
||||
dTag: String,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = assemble(Address.assemble(kind, pubKey, dTag), relay)
|
||||
}
|
||||
}
|
||||
+35
@@ -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.bookmarkList.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
|
||||
sealed interface BookmarkIdTag {
|
||||
fun toTagArray(): Tag
|
||||
|
||||
fun toTagIdOnly(): Tag
|
||||
|
||||
companion object {
|
||||
fun isTagged(tag: Array<String>) = EventBookmark.isTagged(tag) || AddressBookmark.isTagged(tag)
|
||||
|
||||
fun parse(tag: Array<String>): BookmarkIdTag? = EventBookmark.parse(tag) ?: AddressBookmark.parse(tag)
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.bookmarkList.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
class EventBookmark(
|
||||
val eventId: HexKey,
|
||||
val relay: NormalizedRelayUrl? = null,
|
||||
val author: HexKey? = null,
|
||||
) : BookmarkIdTag {
|
||||
fun countMemory(): Long =
|
||||
3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit)
|
||||
eventId.bytesUsedInMemory() +
|
||||
(relay?.url?.bytesUsedInMemory() ?: 0) +
|
||||
(author?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toNEvent(): String = NEvent.create(eventId, author, null, relay)
|
||||
|
||||
override fun toTagArray() = assemble(eventId, relay, author)
|
||||
|
||||
override fun toTagIdOnly() = assemble(eventId, null, null)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "e"
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
@JvmStatic
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
eventId: HexKey,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): EventBookmark? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
return EventBookmark(tag[1], pickRelayHint(tag), pickAuthor(tag))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun pickRelayHint(tag: Array<String>): NormalizedRelayUrl? {
|
||||
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3])
|
||||
if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4])
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun pickAuthor(tag: Array<String>): HexKey? {
|
||||
if (tag.has(2) && tag[2].length == 64) return tag[2]
|
||||
if (tag.has(3) && tag[3].length == 64) return tag[3]
|
||||
if (tag.has(4) && tag[4].length == 64) return tag[4]
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseAsHint(tag: Array<String>): EventIdHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val hint = pickRelayHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return EventIdHint(tag[1], hint)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
author: HexKey?,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author)
|
||||
}
|
||||
}
|
||||
+25
-17
@@ -18,24 +18,32 @@
|
||||
* 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.nip55AndroidSigner.api.foreground.processors
|
||||
package com.vitorpamplona.quartz.nip51Lists.encryption
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
|
||||
class SignResultProcessor(
|
||||
val unsignedEvent: Event,
|
||||
val onReady: (Event) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun consume(intent: Intent) {
|
||||
val foregroundResult = SignResponse.parse(intent, unsignedEvent)
|
||||
suspend fun <T : Event> NostrSigner.signNip51List(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
privateTags: Array<Array<String>>,
|
||||
) = sign<T>(
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = PrivateTagsInContent.encryptNip44(privateTags, this),
|
||||
)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<SignResult>) {
|
||||
onReady(foregroundResult.result.event)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun <T : Event> NostrSignerSync.signNip51List(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
privateTags: Array<Array<String>>,
|
||||
) = sign<T>(
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = PrivateTagsInContent.encryptNip44(privateTags, this),
|
||||
)
|
||||
+52
-20
@@ -20,44 +20,76 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip51Lists.encryption
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
|
||||
class PrivateTagsInContent {
|
||||
companion object {
|
||||
fun decode(content: String) = JsonMapper.mapper.readValue<Array<Array<String>>>(content)
|
||||
|
||||
fun encode(privateTags: Array<Array<String>>) = JsonMapper.mapper.writeValueAsString(privateTags)
|
||||
fun encode(privateTags: Array<Array<String>>): String = JsonMapper.mapper.writeValueAsString(privateTags)
|
||||
|
||||
fun decrypt(
|
||||
suspend fun decrypt(
|
||||
content: String,
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
signer.decrypt(content, signer.pubKey) {
|
||||
onReady(decode(it))
|
||||
): TagArray {
|
||||
if (content.isBlank()) return emptyArray()
|
||||
val json = signer.decrypt(content, signer.pubKey)
|
||||
return try {
|
||||
decode(json)
|
||||
} catch (e: JsonParseException) {
|
||||
Log.w("DraftEvent", "Unable to parse inner event of a draft: $json")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun encryptNip04(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
): String =
|
||||
signer.nip04Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
|
||||
signer.pubKey,
|
||||
)
|
||||
|
||||
suspend fun encryptNip44(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
): String =
|
||||
signer.nip44Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
|
||||
signer.pubKey,
|
||||
)
|
||||
|
||||
suspend fun decrypt(
|
||||
content: String,
|
||||
signer: NostrSignerSync,
|
||||
): TagArray {
|
||||
val json = signer.decrypt(content, signer.pubKey)
|
||||
return decode(json)
|
||||
}
|
||||
|
||||
fun encryptNip04(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) = signer.nip04Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
|
||||
signer.pubKey,
|
||||
onReady,
|
||||
)
|
||||
signer: NostrSignerSync,
|
||||
): String =
|
||||
signer.nip04Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
|
||||
signer.pubKey,
|
||||
)
|
||||
|
||||
fun encryptNip44(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) = signer.nip44Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
|
||||
signer.pubKey,
|
||||
onReady,
|
||||
)
|
||||
signer: NostrSignerSync,
|
||||
): String =
|
||||
signer.nip44Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
|
||||
signer.pubKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+75
-49
@@ -18,19 +18,27 @@
|
||||
* 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
|
||||
package com.vitorpamplona.quartz.nip51Lists.followList
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
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.NameTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.util.UUID
|
||||
import kotlin.collections.plus
|
||||
|
||||
@Immutable
|
||||
class FollowListEvent(
|
||||
@@ -40,102 +48,120 @@ class FollowListEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun pubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PubKeyHintProvider {
|
||||
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
|
||||
|
||||
fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
|
||||
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
|
||||
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||
|
||||
fun nameOrTitle() = name() ?: title()
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
|
||||
fun follows() = tags.follows()
|
||||
|
||||
fun followIds() = tags.followIds()
|
||||
|
||||
fun followIdSet() = tags.followIdSet()
|
||||
|
||||
companion object {
|
||||
const val KIND = 39089
|
||||
const val ALT = "List of people to follow"
|
||||
|
||||
fun createListWithUser(
|
||||
suspend fun create(
|
||||
name: String,
|
||||
pubKeyHex: String,
|
||||
person: UserTag,
|
||||
signer: NostrSigner,
|
||||
dTag: String = UUID.randomUUID().toString(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FollowListEvent) -> Unit,
|
||||
) {
|
||||
): FollowListEvent =
|
||||
create(
|
||||
content = "",
|
||||
tags = arrayOf(arrayOf("d", name), arrayOf("p", pubKeyHex)),
|
||||
name = name,
|
||||
people = listOf(person),
|
||||
signer = signer,
|
||||
dTag = dTag,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun addUsers(
|
||||
suspend fun addUsers(
|
||||
earlierVersion: FollowListEvent,
|
||||
listPubKeyHex: List<String>,
|
||||
people: List<UserTag>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FollowListEvent) -> Unit,
|
||||
) {
|
||||
create(
|
||||
): FollowListEvent =
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags.plus(
|
||||
listPubKeyHex.map { arrayOf("p", it) },
|
||||
),
|
||||
tags = earlierVersion.tags.plus(people.map { it.toTagArray() }),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun addUser(
|
||||
suspend fun add(
|
||||
earlierVersion: FollowListEvent,
|
||||
pubKeyHex: String,
|
||||
person: UserTag,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FollowListEvent) -> Unit,
|
||||
) = addUsers(earlierVersion, listOf(pubKeyHex), signer, createdAt, onReady)
|
||||
) = addUsers(earlierVersion, listOf(person), signer, createdAt)
|
||||
|
||||
fun removeUser(
|
||||
suspend fun remove(
|
||||
earlierVersion: FollowListEvent,
|
||||
pubKeyHex: String,
|
||||
person: UserTag,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FollowListEvent) -> Unit,
|
||||
) {
|
||||
create(
|
||||
): FollowListEvent =
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size > 1 && !(it[0] == "p" && it[1] == pubKeyHex) }
|
||||
.toTypedArray(),
|
||||
tags = earlierVersion.tags.remove(person.toTagIdOnly()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FollowListEvent) -> Unit,
|
||||
) {
|
||||
): FollowListEvent {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
name: String,
|
||||
people: List<UserTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
dTag: String = UUID.randomUUID().toString(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): FollowListEvent {
|
||||
val template = build(name, people, dTag, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
fun build(
|
||||
name: String,
|
||||
people: List<UserTag> = emptyList(),
|
||||
dTag: String = UUID.randomUUID().toString(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<FollowListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(
|
||||
kind = KIND,
|
||||
description = "",
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
dTag(dTag)
|
||||
alt(ALT)
|
||||
name(name)
|
||||
people(people)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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.followList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||
|
||||
fun TagArrayBuilder<FollowListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
||||
|
||||
fun TagArrayBuilder<FollowListEvent>.people(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() })
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.followList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
|
||||
fun TagArray.follows() = mapNotNull(UserTag::parse)
|
||||
|
||||
fun TagArray.followIds() = mapNotNull(UserTag::parseKey)
|
||||
|
||||
fun TagArray.followIdSet() = mapNotNullTo(mutableSetOf(), UserTag::parseKey)
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 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.geohashList
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.NostrSignerSync
|
||||
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.geohash.GeoHashTag
|
||||
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.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class GeohashListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun publicGeohashes() = tags.geohashList()
|
||||
|
||||
suspend fun decryptPrivateGeohashes(signer: NostrSigner) = privateTags(signer)?.geohashList()
|
||||
|
||||
suspend fun decryptGeohashes(signer: NostrSigner): List<String> = publicGeohashes() + (decryptPrivateGeohashes(signer) ?: emptyList())
|
||||
|
||||
companion object {
|
||||
const val KIND = 10081
|
||||
const val ALT = "Geohash List"
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
suspend fun create(
|
||||
geohash: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = create(
|
||||
geohashes = listOf(geohash),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun create(
|
||||
geohashes: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent =
|
||||
if (isPrivate) {
|
||||
create(
|
||||
publicGeohashes = emptyList(),
|
||||
privateGeohashes = geohashes,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
publicGeohashes = geohashes,
|
||||
privateGeohashes = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: GeohashListEvent,
|
||||
geohash: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = add(
|
||||
earlierVersion = earlierVersion,
|
||||
geohashes = listOf(geohash),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: GeohashListEvent,
|
||||
geohashes: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent {
|
||||
val geohashTags = geohashes.map { GeoHashTag.assembleSingle(it) }
|
||||
return if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
tags = earlierVersion.tags,
|
||||
privateTags = privateTags.removeAny(geohashTags) + geohashTags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.removeAny(geohashTags) + geohashTags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: GeohashListEvent,
|
||||
geohash: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(GeoHashTag.assembleSingle(geohash)),
|
||||
tags = earlierVersion.tags.remove(GeoHashTag.assembleSingle(geohash)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
tags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
|
||||
tags = tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent {
|
||||
val newTags =
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
publicGeohashes: List<String> = emptyList(),
|
||||
privateGeohashes: List<String> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent {
|
||||
val template = build(publicGeohashes, privateGeohashes, signer, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
fun create(
|
||||
publicGeohashes: List<String> = emptyList(),
|
||||
privateGeohashes: List<String> = emptyList(),
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent {
|
||||
val privateTagArray = publicGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray()
|
||||
val publicTagArray = privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray() + AltTag.assemble(ALT)
|
||||
return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicGeohashes: List<String> = emptyList(),
|
||||
privateGeohashes: List<String> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<GeohashListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<GeohashListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip04(privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
geohashes(publicGeohashes)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -18,9 +18,11 @@
|
||||
* 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.locations
|
||||
package com.vitorpamplona.quartz.nip51Lists.geohashList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
||||
|
||||
fun TagArrayBuilder<GeohashListEvent>.followGeohash(geohash: String) = add(GeoHashTag.assembleSingle(geohash))
|
||||
|
||||
fun TagArrayBuilder<GeohashListEvent>.geohashes(geohash: List<String>) = addAll(geohash.map { GeoHashTag.assembleSingle(it) })
|
||||
@@ -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.geohashList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
||||
|
||||
fun TagArray.geohashList() = mapNotNull(GeoHashTag::parse)
|
||||
|
||||
fun TagArray.geohashSet() = mapNotNullTo(mutableSetOf(), GeoHashTag::parse)
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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.hashtagList
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.NostrSignerSync
|
||||
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.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.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.collections.map
|
||||
|
||||
@Immutable
|
||||
class HashtagListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun publicHashtags() = tags.mapNotNull(HashtagTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10015
|
||||
const val ALT = "Hashtag List"
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
suspend fun create(
|
||||
hashtag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = create(
|
||||
hashtags = listOf(hashtag),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun create(
|
||||
hashtags: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent =
|
||||
if (isPrivate) {
|
||||
create(
|
||||
publicHashtags = emptyList(),
|
||||
privateHashtags = hashtags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
publicHashtags = hashtags,
|
||||
privateHashtags = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: HashtagListEvent,
|
||||
hashtag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = add(
|
||||
earlierVersion = earlierVersion,
|
||||
hashtags = listOf(hashtag),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: HashtagListEvent,
|
||||
hashtags: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent {
|
||||
val hashtags = hashtags.map { HashtagTag.assemble(it) }
|
||||
return if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
tags = earlierVersion.tags,
|
||||
privateTags = privateTags.removeAny(hashtags) + hashtags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.removeAny(hashtags) + hashtags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: HashtagListEvent,
|
||||
hashtag: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(HashtagTag.assemble(hashtag)),
|
||||
tags = earlierVersion.tags.remove(HashtagTag.assemble(hashtag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
tags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
|
||||
tags = tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent {
|
||||
val newTags =
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
publicHashtags: List<String> = emptyList(),
|
||||
privateHashtags: List<String> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent {
|
||||
val template = build(publicHashtags, privateHashtags, signer, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
fun create(
|
||||
publicHashtags: List<String> = emptyList(),
|
||||
privateHashtags: List<String> = emptyList(),
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent {
|
||||
val privateTagArray = publicHashtags.map { HashtagTag.assemble(it) }.toTypedArray()
|
||||
val publicTagArray = privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray() + AltTag.assemble(ALT)
|
||||
return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicHashtags: List<String> = emptyList(),
|
||||
privateHashtags: List<String> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<HashtagListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<HashtagListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip04(privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
hashtags(publicHashtags)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -18,9 +18,12 @@
|
||||
* 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.interests
|
||||
package com.vitorpamplona.quartz.nip51Lists.hashtagList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
|
||||
|
||||
fun TagArrayBuilder<HashtagListEvent>.followHashTag(hashtag: String) = add(HashtagTag.assemble(hashtag))
|
||||
|
||||
fun TagArrayBuilder<HashtagListEvent>.hashtags(hashtags: List<String>) = addAll(hashtags.map { HashtagTag.assemble(it) })
|
||||
@@ -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.hashtagList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
|
||||
fun TagArray.hashtagList() = mapNotNull(HashtagTag::parse)
|
||||
|
||||
fun TagArray.hashtagSet() = mapNotNullTo(mutableSetOf(), HashtagTag::parse)
|
||||
-220
@@ -1,220 +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.nip51Lists.interests
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Immutable
|
||||
class HashtagListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var publicAndPrivateHashtagCache: Set<String>? = null
|
||||
|
||||
fun publicHashtags() = tags.mapNotNull(HashtagTag::parse)
|
||||
|
||||
fun publicAndCachedPrivateHashtags() = publicHashtags().toSet() + (publicAndPrivateHashtagCache ?: emptySet())
|
||||
|
||||
fun publicAndPrivateHashtag(
|
||||
signer: NostrSigner,
|
||||
onReady: (Set<String>) -> Unit,
|
||||
) {
|
||||
publicAndPrivateHashtagCache?.let { eventList ->
|
||||
onReady(eventList)
|
||||
return
|
||||
}
|
||||
|
||||
mergeTagList(signer) {
|
||||
val set = it.mapNotNull(HashtagTag::parse).toSet()
|
||||
publicAndPrivateHashtagCache = set
|
||||
onReady(set)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun publicAndPrivateHashtag(signer: NostrSigner): Set<String>? {
|
||||
publicAndPrivateHashtagCache?.let { return it }
|
||||
|
||||
return tryAndWait { continuation ->
|
||||
publicAndPrivateHashtag(signer) { privateTagList ->
|
||||
continuation.resume(privateTagList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10015
|
||||
const val ALT = "Hashtag List"
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
private fun createHashtagBase(
|
||||
tags: Array<Array<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.create(
|
||||
tags,
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun createHashtag(
|
||||
hashtag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) = createHashtagBase(
|
||||
tags = arrayOf(HashtagTag.assemble(hashtag)),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun createHashtags(
|
||||
hashtags: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) = createHashtagBase(
|
||||
tags = HashtagTag.assemble(hashtags).toTypedArray(),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun removeHashtag(
|
||||
earlierVersion: HashtagListEvent,
|
||||
hashtag: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.removeAll(
|
||||
earlierVersion,
|
||||
HashtagTag.assemble(hashtag),
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addHashtagBase(
|
||||
earlierVersion: HashtagListEvent,
|
||||
newTags: Array<Array<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.addAll(
|
||||
earlierVersion,
|
||||
newTags,
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun addHashtag(
|
||||
earlierVersion: HashtagListEvent,
|
||||
hashtag: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) = addHashtagBase(
|
||||
earlierVersion,
|
||||
arrayOf(HashtagTag.assemble(hashtag)),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
fun addHashtags(
|
||||
earlierVersion: HashtagListEvent,
|
||||
hashtags: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) = addHashtagBase(
|
||||
earlierVersion,
|
||||
HashtagTag.assemble(hashtags).toTypedArray(),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
private fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
hashtags: List<String>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): HashtagListEvent? {
|
||||
val tags = HashtagTag.assemble(hashtags).toTypedArray()
|
||||
return signer.sign(createdAt, KIND, tags, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
-220
@@ -1,220 +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.nip51Lists.locations
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Immutable
|
||||
class GeohashListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient var publicAndPrivateGeohashCache: Set<String>? = null
|
||||
|
||||
fun publicGeohashes() = tags.mapNotNull(GeoHashTag::parse)
|
||||
|
||||
fun publicAndCachedPrivateGeohash() = publicGeohashes().toSet() + (publicAndPrivateGeohashCache ?: emptySet())
|
||||
|
||||
fun publicAndPrivateGeohash(
|
||||
signer: NostrSigner,
|
||||
onReady: (Set<String>) -> Unit,
|
||||
) {
|
||||
publicAndPrivateGeohashCache?.let { eventList ->
|
||||
onReady(eventList)
|
||||
return
|
||||
}
|
||||
|
||||
mergeTagList(signer) {
|
||||
val set = it.mapNotNull(GeoHashTag::parse).toSet()
|
||||
publicAndPrivateGeohashCache = set
|
||||
onReady(set)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun publicAndPrivateGeohash(signer: NostrSigner): Set<String>? {
|
||||
publicAndPrivateGeohashCache?.let { return it }
|
||||
|
||||
return tryAndWait { continuation ->
|
||||
publicAndPrivateGeohash(signer) { privateTagList ->
|
||||
continuation.resume(privateTagList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10081
|
||||
const val ALT = "Geohash List"
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
private fun createGeohashBase(
|
||||
tags: Array<Array<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.create(
|
||||
tags,
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun createGeohash(
|
||||
geohash: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) = createGeohashBase(
|
||||
tags = arrayOf(GeoHashTag.assembleSingle(geohash)),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun createGeohashs(
|
||||
geohashs: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) = createGeohashBase(
|
||||
tags = geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray(),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
|
||||
fun removeGeohash(
|
||||
earlierVersion: GeohashListEvent,
|
||||
geohash: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.removeAll(
|
||||
earlierVersion,
|
||||
GeoHashTag.assembleSingle(geohash),
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGeohashBase(
|
||||
earlierVersion: GeohashListEvent,
|
||||
newTags: Array<Array<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
PrivateTagArrayBuilder.addAll(
|
||||
earlierVersion,
|
||||
newTags,
|
||||
isPrivate,
|
||||
signer,
|
||||
) { encryptedContent, newTags ->
|
||||
create(encryptedContent, newTags, signer, createdAt, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun addGeohash(
|
||||
earlierVersion: GeohashListEvent,
|
||||
geohash: String,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) = addGeohashBase(
|
||||
earlierVersion,
|
||||
arrayOf(GeoHashTag.assembleSingle(geohash)),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
fun addGeohashs(
|
||||
earlierVersion: GeohashListEvent,
|
||||
geohashs: List<String>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) = addGeohashBase(
|
||||
earlierVersion,
|
||||
geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray(),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
|
||||
private fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
geohashs: List<String>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent? {
|
||||
val tags = geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray()
|
||||
return signer.sign(createdAt, KIND, tags, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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.muteList
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.addressables.Address
|
||||
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.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.collections.plus
|
||||
|
||||
@Immutable
|
||||
class MuteListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PubKeyHintProvider {
|
||||
override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey)
|
||||
|
||||
fun countMutes() = tags.count(MuteTag::isTagged)
|
||||
|
||||
fun publicMutes(): List<MuteTag> = tags.mapNotNull(MuteTag::parse)
|
||||
|
||||
suspend fun privateMutes(signer: NostrSigner): List<MuteTag>? = privateTags(signer)?.mapNotNull(MuteTag::parse)
|
||||
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
companion object {
|
||||
const val KIND = 10000
|
||||
const val FIXED_D_TAG = ""
|
||||
const val ALT = "Mute List"
|
||||
|
||||
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun blockListFor(pubKeyHex: HexKey): String = "10000:$pubKeyHex:"
|
||||
|
||||
suspend fun create(
|
||||
mute: MuteTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): MuteListEvent =
|
||||
if (isPrivate) {
|
||||
create(
|
||||
publicMutes = emptyList(),
|
||||
privateMutes = listOf(mute),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
publicMutes = listOf(mute),
|
||||
privateMutes = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: MuteListEvent,
|
||||
mute: MuteTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): MuteListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
publicTags = earlierVersion.tags,
|
||||
privateTags = privateTags.plus(mute.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(mute.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: MuteListEvent,
|
||||
mute: MuteTag,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): MuteListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
return resign(
|
||||
privateTags = privateTags.remove(mute.toTagIdOnly()),
|
||||
publicTags = earlierVersion.tags.remove(mute.toTagIdOnly()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
publicTags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
|
||||
tags = publicTags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): MuteListEvent {
|
||||
val newTags =
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
publicMutes: List<MuteTag> = emptyList(),
|
||||
privateMutes: List<MuteTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): MuteListEvent {
|
||||
val template = build(publicMutes, privateMutes, signer, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
publicMutes: List<MuteTag> = emptyList(),
|
||||
privateMutes: List<MuteTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<MuteListEvent>.() -> Unit = {},
|
||||
) = eventTemplate<MuteListEvent>(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip04(privateMutes.map { it.toTagArray() }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
mutes(publicMutes)
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.muteList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
|
||||
fun TagArrayBuilder<MuteListEvent>.mutes(mutes: List<MuteTag>) = addAll(mutes.map { it.toTagArray() })
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.muteList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
|
||||
|
||||
fun TagArray.mutedUsersAndWords() = mapNotNull(MuteTag::parse)
|
||||
|
||||
fun TagArray.mutedUsers() = mapNotNull(UserTag::parse)
|
||||
|
||||
fun TagArray.mutedUserIds() = mapNotNull(UserTag::parseKey)
|
||||
|
||||
fun TagArray.mutedUserIdSet() = mapNotNullTo(mutableSetOf(), UserTag::parseKey)
|
||||
|
||||
fun TagArray.mutedWords() = mapNotNull(WordTag::parse)
|
||||
|
||||
fun TagArray.mutedWordSet() = mapNotNullTo(mutableSetOf(), WordTag::parse)
|
||||
@@ -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.muteList.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
|
||||
sealed interface MuteTag {
|
||||
fun toTagArray(): Tag
|
||||
|
||||
fun toTagIdOnly(): Tag
|
||||
|
||||
companion object {
|
||||
fun isTagged(tag: Array<String>) = WordTag.isTagged(tag) || UserTag.isTagged(tag)
|
||||
|
||||
fun parse(tag: Array<String>): MuteTag? = WordTag.parse(tag) ?: UserTag.parse(tag)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user