Massive refactoring of Quartz to:

- Define each tag in their own class.
- Allow extension functions to additional responsibilities to other classes
- Migrate from hardcoded tag filters in events to the Tag's parser and assemble functions.
- Migrate hardcoded event.create to builders that use extension functions
- Restructures threading infrastructure for NIP-10
- Decouple the event signing from the Event building functions via event templates
- Create classes to represent Tags and TagArrays and use extension functions to add domain-related methods to the tag array of each nip.
- Uses external functions on event template builders to better point to which functions and which tags can be used in which event kinds.
- Separates Event kinds in packages inside each nip.
- Improves support for NIP-89
- Correctly establishes which imeta params can be used in each nip (video, picture, files)
- Decouples the iMeta builder from any nip.
- Fixes mute list word and user removal when inserted from a different client.
- Migrates the Account class to avoiding having to build each Event inside of it
This commit is contained in:
Vitor Pamplona
2025-02-13 17:39:13 -05:00
parent 0f3b3c4b2d
commit 9c21a8a7da
614 changed files with 15380 additions and 7132 deletions
@@ -21,7 +21,7 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import org.junit.Assert.assertEquals
@@ -24,8 +24,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hasValidSignature
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.runBlocking
@@ -48,7 +48,7 @@ class LargeDBSignatureCheck {
var counter = 0
eventArray.forEach {
assertTrue(it.hasValidSignature())
assertTrue(it.verify())
counter++
}
@@ -69,7 +69,7 @@ class LargeDBSignatureCheck {
var counter = 0
eventArray.forEach {
if (it.sig != "") {
assertTrue(it.hasValidSignature())
assertTrue(it.verify())
}
counter++
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip01Core
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.sha256Hash
import fr.acinq.secp256k1.Secp256k1
import org.junit.Assert.assertEquals
@@ -20,20 +20,19 @@
*/
package com.vitorpamplona.quartz.nip03Timestamp.ots
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class OtsTest {
val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}"
@@ -44,37 +43,39 @@ class OtsTest {
@Test
fun verifyNostrEvent() {
val ots = Event.fromJson(otsEvent) as OtsEvent
println(ots.info())
println(OtsResolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify())
}
@Test
fun verifyNostrEvent2() {
val ots = Event.Companion.fromJson(otsEvent2) as OtsEvent
println(ots.info())
println(OtsResolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify())
}
@Test
fun verifyNostrPendingEvent() {
val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent
println(ots.info())
println(OtsResolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify())
val eventId =
ots.digestEvent() ?: run {
ots.digestEventId() ?: run {
fail("Should not be null")
return
}
val upgraded = OtsEvent.upgrade(ots.content, eventId)
val upgraded = OtsEvent.upgrade(ots.otsByteArray(), eventId)
assertNotNull(upgraded)
val signer = NostrSignerInternal(KeyPair())
var newOts: OtsEvent? = null
val countDownLatch = CountDownLatch(1)
OtsEvent.create(eventId, upgraded, signer) {
signer.sign(OtsEvent.build(eventId, upgraded!!)) {
newOts = it
countDownLatch.countDown()
}
@@ -82,7 +83,7 @@ class OtsTest {
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
println(newOts!!.toJson())
println(newOts!!.info())
println(OtsResolver.info(newOts!!.otsByteArray()))
assertEquals(1708879025L, newOts!!.verify())
}
@@ -94,7 +95,7 @@ class OtsTest {
val countDownLatch = CountDownLatch(1)
OtsEvent.create(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest), signer) {
signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest))) {
ots = it
countDownLatch.countDown()
}
@@ -102,7 +103,7 @@ class OtsTest {
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
println(ots!!.toJson())
println(ots!!.info())
println(OtsResolver.info(ots!!.otsByteArray()))
assertEquals(null, ots!!.verify())
}
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.quartz.nip04Dm
import com.vitorpamplona.quartz.nip01Core.Nip01
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import fr.acinq.secp256k1.Secp256k1
@@ -29,10 +29,10 @@ import junit.framework.TestCase.assertTrue
import org.junit.Test
import java.security.SecureRandom
class Nip04Test {
class Nip04EncryptionTest {
private val random = SecureRandom()
private val nip01 = Nip01(Secp256k1.get(), random)
private val nip04 = Nip04(Secp256k1.get(), random)
private val nip04 = Nip04Encryption(Secp256k1.get(), random)
val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray()
val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray()
@@ -89,17 +89,17 @@ class Nip04Test {
@Test
fun isNIP04Encode() {
assertTrue(Nip04.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
assertTrue(Nip04.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(Nip04Encryption.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
assertTrue(Nip04Encryption.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(
Nip04.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
Nip04Encryption.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
)
}
@Test
fun isNIP04EncodeWithBug() {
assertTrue(
Nip04.isNIP04(
Nip04Encryption.isNIP04(
"QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" +
"TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" +
"xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" +
@@ -21,82 +21,49 @@
package com.vitorpamplona.quartz.nip10Notes
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip01Core.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CitationTests {
val json =
"""
{
"content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
"created_at": 1683596206,
"id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
"kind": 1,
"pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
"sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
"tags": [
[
"e",
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"",
"root"
],
[
"e",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
"",
"reply"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"
],
[
"p",
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"
],
[
"p",
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"
],
[
"p",
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"
],
[
"p",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
],
[
"p",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"
],
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://nostr.wine/"
]
}
"""
val note =
TextNoteEvent(
"98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
"46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
1683596206,
arrayOf(
arrayOf("e", "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", "", "root"),
arrayOf("e", "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", "", "reply"),
arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
arrayOf("p", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"),
arrayOf("p", "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"),
arrayOf("p", "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"),
arrayOf("p", "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"),
arrayOf("p", "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"),
arrayOf("p", "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"),
arrayOf("p", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
),
"Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
"4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
)
@Test
fun parseEvent() {
val event = EventMapper.fromJson(json) as TextNoteEvent
fun verifyEvent() {
assertTrue(note.verify())
}
@Test
fun testCitedUsers() {
val expectedCitations =
setOf(
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
@@ -105,6 +72,75 @@ class CitationTests {
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
)
assertEquals(expectedCitations, event.citedUsers())
assertEquals(expectedCitations, note.citedUsers())
}
@Test
fun testTagsWithoutCitations() {
val expectedTagsWithoutCitations =
listOf(
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
)
assertEquals(expectedTagsWithoutCitations, note.tagsWithoutCitations())
}
@Test
fun testTaggedEventIds() {
val eventIds =
listOf(
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
)
assertEquals(eventIds, note.taggedEventIds())
}
@Test
fun testTaggedEvents() {
val eventIds =
listOf(
ETag("27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3"),
ETag("be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc"),
)
assertEquals(eventIds, note.taggedEvents())
}
@Test
fun testTaggedUsers() {
val eventIds =
listOf(
PTag("22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
PTag("22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
PTag("3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"),
PTag("ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"),
PTag("0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"),
PTag("8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"),
PTag("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"),
PTag("4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"),
PTag("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
)
assertEquals(eventIds, note.taggedUsers())
}
@Test
fun testTaggedUserIds() {
val eventIds =
listOf(
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24",
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2",
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac",
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
)
assertEquals(eventIds, note.taggedUserIds())
}
}
@@ -0,0 +1,340 @@
/**
* Copyright (c) 2024 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.nip10Notes
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ThreadingTests {
@Test
fun testLegacyEvent() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
arrayOf("p", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
arrayOf("e", "89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d"),
arrayOf("e", "090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051"),
arrayOf("e", "567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976"),
arrayOf("e", "49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4"),
arrayOf("e", "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64"),
arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631"),
arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(null, note.markedReply())
assertEquals(expectedReply, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val replyTos =
listOf(
"89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d",
"090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051",
"567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976",
"49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4",
"5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64",
"bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631",
"b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c",
)
assertEquals(emptyList<String>(), note.markedReplyTos())
assertEquals(replyTos, note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d", null, MarkedETag.MARKER.ROOT)
assertEquals(null, note.markedRoot())
assertEquals(expectedRoot, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(replyTos, note.tagsWithoutCitations())
}
@Test
fun testMarkedEvent() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
arrayOf("e", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", "", "root"),
arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", "", "reply"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY)
val expectedReplyWrong = MarkedETag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(expectedReply, note.markedReply())
assertEquals(expectedReplyWrong, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val markedReplies =
listOf(
"bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631",
"b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c",
)
val unmarkedReplies =
listOf(
"77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7",
)
assertEquals(markedReplies, note.markedReplyTos())
assertEquals(unmarkedReplies, note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", null, MarkedETag.MARKER.ROOT)
val expectedRootWrong = MarkedETag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", null, MarkedETag.MARKER.ROOT)
assertEquals(expectedRoot, note.markedRoot())
assertEquals(expectedRootWrong, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(markedReplies, note.tagsWithoutCitations())
}
@Test
fun testMarkedInverted() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", "wss://goiaba.com"),
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
arrayOf("p", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
arrayOf("e", "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", "", "reply"),
arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", "wss://banana.com", "root", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", "wss://goiaba.com"),
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", null, MarkedETag.MARKER.REPLY)
val expectedReplyWrong = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(expectedReply, note.markedReply())
assertEquals(expectedReplyWrong, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val markedReplies =
listOf(
"bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631",
"5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64",
)
val unmarkedReplies =
listOf(
"b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c",
)
assertEquals(markedReplies, note.markedReplyTos())
assertEquals(unmarkedReplies, note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", null, MarkedETag.MARKER.ROOT)
val expectedRootWrong = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.ROOT)
assertEquals(expectedRoot, note.markedRoot())
assertEquals(expectedRootWrong, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(markedReplies, note.tagsWithoutCitations())
}
@Test
fun testOnlyRoot() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"),
arrayOf("e", "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", "", "root", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(null, note.markedReply())
assertEquals(null, note.unmarkedReply())
assertEquals(null, note.reply())
val replyTos =
listOf(
"9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590",
)
assertEquals(replyTos, note.markedReplyTos())
assertEquals(emptyList<String>(), note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.ROOT, "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec")
assertEquals(expectedRoot, note.markedRoot())
assertEquals(null, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(replyTos, note.tagsWithoutCitations())
}
@Test
fun testOnlyReply() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"),
arrayOf("e", "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", "", "reply", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(expectedReply, note.markedReply())
assertEquals(null, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val replyTos =
listOf(
"9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590",
)
assertEquals(replyTos, note.markedReplyTos())
assertEquals(emptyList<String>(), note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.ROOT, "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec")
assertEquals(null, note.markedRoot())
assertEquals(null, note.unmarkedRoot())
assertEquals(null, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(replyTos, note.tagsWithoutCitations())
}
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip17Dm
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip17Dm
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -22,11 +22,11 @@ package com.vitorpamplona.quartz.nip19Bech32
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hasValidSignature
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.utils.Hex
@@ -52,7 +52,9 @@ class NIP19EmbedTests {
val countDownLatch = CountDownLatch(1)
TextNoteEvent.create("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size.", isDraft = false, signer = signer) {
signer.sign(
TextNoteEvent.build("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size."),
) {
textNote = it
countDownLatch.countDown()
}
@@ -67,7 +69,7 @@ class NIP19EmbedTests {
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.hasValidSignature())
assertTrue(decodedNote.verify())
assertEquals(textNote!!.toJson(), decodedNote.toJson())
}
@@ -83,7 +85,7 @@ class NIP19EmbedTests {
val countDownLatch = CountDownLatch(1)
FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) {
signer.sign(FhirResourceEvent.build(visionPrescriptionFhir)) {
eyeglassesPrescriptionEvent = it
countDownLatch.countDown()
}
@@ -99,7 +101,7 @@ class NIP19EmbedTests {
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.hasValidSignature())
assertTrue(decodedNote.verify())
assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson())
}
@@ -115,7 +117,7 @@ class NIP19EmbedTests {
val countDownLatch = CountDownLatch(1)
FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle, signer = signer) {
signer.sign(FhirResourceEvent.build(visionPrescriptionBundle)) {
eyeglassesPrescriptionEvent = it
countDownLatch.countDown()
}
@@ -131,7 +133,7 @@ class NIP19EmbedTests {
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.hasValidSignature())
assertTrue(decodedNote.verify())
assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson())
}
@@ -147,7 +149,7 @@ class NIP19EmbedTests {
val countDownLatch = CountDownLatch(1)
FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle2, signer = signer) {
signer.sign(FhirResourceEvent.build(visionPrescriptionBundle2)) {
eyeglassesPrescriptionEvent = it
countDownLatch.countDown()
}
@@ -163,7 +165,7 @@ class NIP19EmbedTests {
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.hasValidSignature())
assertTrue(decodedNote.verify())
assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson())
}
@@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.nip44Encryption
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.Nip01
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.sha256Hash
@@ -21,10 +21,10 @@
package com.vitorpamplona.quartz.nip46RemoteSigner
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase.assertEquals
import org.junit.Ignore
@@ -219,7 +219,7 @@ internal class Nip46Test {
@Test
fun testRelaysResponse() {
val expected = BunkerResponseGetRelays(relays = mapOf("url" to ContactListEvent.ReadWrite(true, false)))
val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false)))
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
@@ -21,8 +21,8 @@
package com.vitorpamplona.quartz.nip57Zaps
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.toHexKey
@@ -22,16 +22,17 @@ package com.vitorpamplona.quartz.nip59GiftWraps
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.KeyPair
import com.vitorpamplona.quartz.nip01Core.checkSignature
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent
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 org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
@@ -54,9 +55,11 @@ class GiftWrapEventTest {
// Requires 3 tests
val countDownLatch = CountDownLatch(3)
NIP17Factory().createMsgNIP17(
message,
listOf(receiver.pubKey),
NIP17Factory().createMessageNIP17(
ChatMessageEvent.build(
message,
listOf(PTag(receiver.pubKey, null)),
),
sender,
) { events ->
countDownLatch.countDown()
@@ -114,9 +117,11 @@ class GiftWrapEventTest {
val countDownLatch = CountDownLatch(receivers.size + 2)
NIP17Factory().createMsgNIP17(
message,
receivers.map { it.pubKey },
NIP17Factory().createMessageNIP17(
ChatMessageEvent.build(
message,
receivers.map { PTag(it.pubKey, null) },
),
sender,
) { events ->
countDownLatch.countDown()
@@ -167,11 +172,11 @@ class GiftWrapEventTest {
var giftWrapEventToSender: GiftWrapEvent? = null
var giftWrapEventToReceiver: GiftWrapEvent? = null
ChatMessageEvent.create(
msg = "Hi There!",
isDraft = false,
to = listOf(receiver.pubKey),
signer = sender,
sender.sign(
ChatMessageEvent.build(
msg = "Hi There!",
to = listOf(PTag(receiver.pubKey, null)),
),
) { senderMessage ->
// MsgFor the Receiver
@@ -306,11 +311,11 @@ class GiftWrapEventTest {
var giftWrapEventToReceiverA: GiftWrapEvent? = null
var giftWrapEventToReceiverB: GiftWrapEvent? = null
ChatMessageEvent.create(
msg = "Who is going to the party tonight?",
isDraft = false,
to = listOf(receiverA.pubKey, receiverB.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,
@@ -21,8 +21,9 @@
package com.vitorpamplona.quartz
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.Nip01
import com.vitorpamplona.quartz.nip04Dm.Nip04
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.Nip04Encoder
import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip44Encryption.Nip44
import com.vitorpamplona.quartz.nip44Encryption.Nip44v2
@@ -38,7 +39,7 @@ object CryptoUtils {
val nip01 = Nip01(secp256k1, random)
val nip06 = Nip06(secp256k1)
val nip04 = Nip04(secp256k1, random)
val nip04 = Nip04Encryption(secp256k1, random)
val nip44 = Nip44(secp256k1, random, nip04)
val nip49 = Nip49(secp256k1, random)
@@ -84,7 +85,7 @@ object CryptoUtils {
privateKey: ByteArray,
pubKey: ByteArray,
): String? =
if (Nip04.isNIP04(msg)) {
if (Nip04Encryption.isNIP04(msg)) {
decryptNIP04(msg, privateKey, pubKey)
} else {
decryptNIP44(msg, privateKey, pubKey)
@@ -100,7 +101,7 @@ object CryptoUtils {
fun encryptNIP04(
msg: String,
sharedSecret: ByteArray,
): Nip04.EncryptedInfo = nip04.encrypt(msg, sharedSecret)
): Nip04Encoder = nip04.encrypt(msg, sharedSecret)
fun decryptNIP04(
msg: String,
@@ -109,7 +110,7 @@ object CryptoUtils {
): String = nip04.decrypt(msg, privateKey, pubKey)
fun decryptNIP04(
encryptedInfo: Nip04.EncryptedInfo,
encryptedInfo: Nip04Encoder,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.decrypt(encryptedInfo, privateKey, pubKey)
@@ -22,49 +22,49 @@ package com.vitorpamplona.quartz
import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.blossom.BlossomServersEvent
import com.vitorpamplona.quartz.experimental.audio.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.AudioTrackEvent
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.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.experimental.nip95.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.EventHasher
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelHideMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelMuteUserEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip34Git.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.GitRepositoryEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
@@ -82,8 +82,8 @@ import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
@@ -92,28 +92,27 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.GiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip71Video.VideoViewEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.AppRecommendationEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.nip96FileStorage.FileServersEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
@@ -251,7 +250,6 @@ class EventFactory {
TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig)
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
else -> {
factories[kind]?.let {
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -58,7 +58,7 @@ class BlossomServersEvent(
servers
.map {
arrayOf("server", it)
}.plusElement(AltTagSerializer.toTagArray(ALT))
}.plusElement(AltTag.assemble(ALT))
.toTypedArray()
fun updateRelayList(
@@ -1,88 +0,0 @@
/**
* Copyright (c) 2024 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.audio
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class AudioHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun download() = tags.firstOrNull { it.size > 1 && it[0] == DOWNLOAD_URL }?.get(1)
fun stream() = tags.firstOrNull { it.size > 1 && it[0] == STREAM_URL }?.get(1)
fun wavefrom() =
tags
.firstOrNull { it.size > 1 && it[0] == WAVEFORM }
?.get(1)
?.let { EventMapper.mapper.readValue<List<Int>>(it) }
companion object {
const val KIND = 1808
const val ALT = "Audio header"
private const val DOWNLOAD_URL = "download_url"
private const val STREAM_URL = "stream_url"
private const val WAVEFORM = "waveform"
fun create(
description: String,
downloadUrl: String,
streamUrl: String? = null,
wavefront: String? = null,
sensitiveContent: Boolean? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (AudioHeaderEvent) -> Unit,
) {
val tags =
listOfNotNull(
downloadUrl.let { arrayOf(DOWNLOAD_URL, it) },
streamUrl?.let { arrayOf(STREAM_URL, it) },
wavefront?.let { arrayOf(WAVEFORM, it) },
sensitiveContent?.let {
if (it) {
ContentWarningSerializer.toTagArray()
} else {
null
}
},
AltTagSerializer.toTagArray(ALT),
).toTypedArray()
signer.sign(createdAt, KIND, tags, description, onReady)
}
}
}
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2024 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.audio.header
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.audio.header.tags.DownloadUrlTag
import com.vitorpamplona.quartz.experimental.audio.header.tags.StreamUrlTag
import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class AudioHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun download() = tags.firstNotNullOfOrNull(DownloadUrlTag::parse)
fun stream() = tags.firstNotNullOfOrNull(StreamUrlTag::parse)
fun wavefrom() = tags.firstNotNullOfOrNull(WaveformTag::parse)
companion object {
const val KIND = 1808
const val ALT = "Audio header"
fun build(
description: String,
downloadUrl: String,
streamUrl: String? = null,
wavefront: List<Int>? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AudioHeaderEvent>.() -> Unit = {},
) = eventTemplate(KIND, description, createdAt) {
alt(ALT)
downloadUrl.let { downloadUrl(it) }
streamUrl?.let { streamUrl(it) }
wavefront?.let { wavefront(it) }
initializer()
}
}
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2024 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.audio.header
import com.vitorpamplona.quartz.experimental.audio.header.tags.DownloadUrlTag
import com.vitorpamplona.quartz.experimental.audio.header.tags.StreamUrlTag
import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<AudioHeaderEvent>.downloadUrl(downloadUrlTag: String) = addUnique(DownloadUrlTag.assemble(downloadUrlTag))
fun TagArrayBuilder<AudioHeaderEvent>.streamUrl(streamUrl: String) = addUnique(StreamUrlTag.assemble(streamUrl))
fun TagArrayBuilder<AudioHeaderEvent>.wavefront(wave: List<Int>) = addUnique(WaveformTag.assemble(wave))
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.audio.header.tags
class DownloadUrlTag {
companion object {
const val TAG_NAME = "download_url"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.audio.header.tags
class StreamUrlTag {
companion object {
const val TAG_NAME = "stream_url"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2024 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.audio.header.tags
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
class WaveformTag(
val wave: List<Int>,
) {
fun toTagArray() = assemble(wave)
companion object {
const val TAG_NAME = "waveform"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): WaveformTag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
if (tag[1].isEmpty()) return null
val wave = runCatching { EventMapper.mapper.readValue<List<Int>>(tag[1]) }.getOrNull()
if (wave.isNullOrEmpty()) return null
return WaveformTag(wave)
}
@JvmStatic
fun assemble(wave: List<Int>) = arrayOf(TAG_NAME, EventMapper.mapper.writeValueAsString(wave))
}
}
@@ -18,14 +18,22 @@
* 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.audio
package com.vitorpamplona.quartz.experimental.audio.track
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.audio.track.tags.CoverTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.MediaTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.ParticipantTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.PriceTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.TypeTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID
@Immutable
class AudioTrackEvent(
@@ -36,53 +44,40 @@ class AudioTrackEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) }
fun participants() = tags.mapNotNull(ParticipantTag::parse)
fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
fun type() = tags.firstNotNullOfOrNull(TypeTag::parse)
fun price() = tags.firstOrNull { it.size > 1 && it[0] == PRICE }?.get(1)
fun price() = tags.firstNotNullOfOrNull(PriceTag::parse)
fun cover() = tags.firstOrNull { it.size > 1 && it[0] == COVER }?.get(1)
fun cover() = tags.firstNotNullOfOrNull(CoverTag::parse)
// fun subject() = tags.firstOrNull { it.size > 1 && it[0] == SUBJECT }?.get(1)
fun media() = tags.firstOrNull { it.size > 1 && it[0] == MEDIA }?.get(1)
fun media() = tags.firstNotNullOfOrNull(MediaTag::parse)
companion object {
const val KIND = 31337
const val ALT = "Audio track"
const val ALT_DESCRIPTION = "Audio track"
private const val TYPE = "c"
private const val PRICE = "price"
private const val COVER = "cover"
private const val SUBJECT = "subject"
private const val MEDIA = "media"
fun create(
fun build(
type: String,
media: String,
price: String? = null,
cover: String? = null,
subject: String? = null,
signer: NostrSigner,
dTag: String = UUID.randomUUID().toString(),
createdAt: Long = TimeUtils.now(),
onReady: (AudioTrackEvent) -> Unit,
) {
val tags =
listOfNotNull(
arrayOf(MEDIA, media),
arrayOf(TYPE, type),
price?.let { arrayOf(PRICE, it) },
cover?.let { arrayOf(COVER, it) },
subject?.let { arrayOf(SUBJECT, it) },
AltTagSerializer.toTagArray(ALT),
).toTypedArray()
initializer: TagArrayBuilder<AudioTrackEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
dTag(dTag)
alt(ALT_DESCRIPTION)
signer.sign(createdAt, KIND, tags, "", onReady)
type(type)
media(media)
price?.let { price(it) }
cover?.let { cover(it) }
subject?.let { subject(it) }
initializer()
}
}
}
@Immutable data class Participant(
val key: String,
val role: String?,
)
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2024 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.audio.track
import com.vitorpamplona.quartz.experimental.audio.track.tags.CoverTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.MediaTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.PriceTag
import com.vitorpamplona.quartz.experimental.audio.track.tags.TypeTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip14Subject.SubjectTag
fun TagArrayBuilder<AudioTrackEvent>.price(price: String) = addUnique(PriceTag.assemble(price))
fun TagArrayBuilder<AudioTrackEvent>.cover(coverUrl: String) = addUnique(CoverTag.assemble(coverUrl))
fun TagArrayBuilder<AudioTrackEvent>.media(mediaUrl: String) = addUnique(MediaTag.assemble(mediaUrl))
fun TagArrayBuilder<AudioTrackEvent>.type(type: String) = addUnique(TypeTag.assemble(type))
fun TagArrayBuilder<AudioTrackEvent>.subject(subject: String) = addUnique(SubjectTag.assemble(subject))
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.audio.track.tags
class CoverTag {
companion object {
const val TAG_NAME = "cover"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.audio.track.tags
class MediaTag {
companion object {
const val TAG_NAME = "media"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2024 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.audio.track.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Tag
import com.vitorpamplona.quartz.nip01Core.core.isNotName
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
import com.vitorpamplona.quartz.utils.arrayOfNotNull
@Immutable
data class ParticipantTag(
override val pubKey: String,
override val relayHint: String?,
) : PubKeyReferenceTag {
fun toTagArray() = assemble(pubKey, relayHint)
companion object {
const val TAG_NAME = "p"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Tag): ParticipantTag? {
if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null
if (tag[1].length != 64) return null
return ParticipantTag(tag[1], tag.getOrNull(2))
}
@JvmStatic
fun parseKey(tag: Tag): String? {
if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null
if (tag[1].length != 64) return null
return tag[1]
}
@JvmStatic
fun assemble(
pubkey: HexKey,
relayHint: String?,
) = arrayOfNotNull(TAG_NAME, pubkey, relayHint)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.audio.track.tags
class PriceTag {
companion object {
const val TAG_NAME = "price"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.audio.track.tags
class TypeTag {
companion object {
const val TAG_NAME = "c"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2024 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.bounties
import com.vitorpamplona.quartz.nip01Core.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip10Notes.tags.markedETag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
/**
* This is a huge hack from back in the days...
*/
class BountyAddValueEvent {
companion object {
const val KIND = 1
const val ALT_DESCRIPTION = "Add Value to Bounty"
const val BOUNTY_HASH_TAG = "bounty-added-reward"
fun build(
amount: BigDecimal,
bountyRoot: EventHintBundle<TextNoteEvent>,
bountyRootAuthor: PTag,
createdAt: Long = TimeUtils.now(),
): EventTemplate<TextNoteEvent> {
val tags = TagArrayBuilder<TextNoteEvent>()
tags.markedETag(bountyRoot.toMarkedETag(MarkedETag.MARKER.ROOT))
tags.hashtag(BOUNTY_HASH_TAG)
tags.pTag(bountyRootAuthor)
tags.alt(ALT_DESCRIPTION)
return EventTemplate(createdAt, KIND, tags.build(), amount.toString())
}
}
}
@@ -21,7 +21,15 @@
package com.vitorpamplona.quartz.experimental.bounties
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.firstMapTagged
import java.math.BigDecimal
fun Event.getReward(): BigDecimal? = tags.firstMapTagged("reward") { runCatching { BigDecimal(it[1]) }.getOrNull() }
fun Event.bountyBaseReward(): BigDecimal? = tags.bountyBaseReward()
fun Event.hasAdditionalReward(): Boolean = tags.hasAdditionalReward()
fun Event.addedRewardValue(): BigDecimal? =
if (hasAdditionalReward()) {
runCatching { BigDecimal(content) }.getOrNull()
} else {
null
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2024 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.bounties
import java.math.BigDecimal
class RewardTag {
companion object {
const val TAG_NAME = "reward"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): BigDecimal? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return runCatching { BigDecimal(tag[1]) }.getOrNull()
}
@JvmStatic
fun assemble(amount: BigDecimal) = arrayOf(TAG_NAME, amount.toString())
}
}
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2024 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.bounties
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent.Companion.BOUNTY_HASH_TAG
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
fun TagArray.bountyBaseReward() = this.firstNotNullOfOrNull(RewardTag::parse)
fun TagArray.hasAdditionalReward() = this.isTaggedHash(BOUNTY_HASH_TAG)
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
@@ -100,7 +100,7 @@ class PrivateOutboxRelayListEvent(
companion object {
const val KIND = 10013
val TAGS = arrayOf(AltTagSerializer.toTagArray("Relay list to store private content from this author"))
val TAGS = arrayOf(AltTag.assemble("Relay list to store private content from this author"))
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -64,7 +64,7 @@ class TextNoteModificationEvent(
tags.add(arrayOf("summary", it))
}
tags.add(AltTagSerializer.toTagArray(ALT))
tags.add(AltTag.assemble(ALT))
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2024 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.forks
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
fun BaseThreadedEvent.isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
fun BaseThreadedEvent.forkFromAddress() =
tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
fun BaseThreadedEvent.forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork)
fun BaseThreadedEvent.isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) }
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2024 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.forks
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER
fun MarkedETag.Companion.parseFork(tag: Array<String>): MarkedETag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
if (tag[ORDER_MARKER] != MARKER.FORK.code) return null
// ["e", id hex, relay hint, marker, pubkey]
return MarkedETag(
tag[ORDER_EVT_ID],
tag[ORDER_RELAY],
tag[ORDER_MARKER],
tag.getOrNull(
ORDER_PUBKEY,
),
)
}
@@ -20,28 +20,22 @@
*/
package com.vitorpamplona.quartz.experimental.inlineMetadata
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import kotlin.coroutines.cancellation.CancellationException
class Nip54InlineMetadata {
fun createUrl(header: IMetaTag): String =
createUrl(
header.url,
header.properties,
)
fun createUrl(
url: String,
tags: Map<String, String>,
tags: Map<String, List<String>>,
): String {
val extension =
tags
.mapNotNull {
if (it.key != "url") {
"${it.key}=${URLEncoder.encode(it.value, "utf-8")}"
val value = it.value.firstOrNull()
if (it.key != "url" && value != null) {
"${it.key}=${URLEncoder.encode(value, "utf-8")}"
} else {
null
}
@@ -20,23 +20,12 @@
*/
package com.vitorpamplona.quartz.experimental.interactiveStories
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.buildHashtagTags
import com.vitorpamplona.quartz.nip10Notes.content.buildUrlRefs
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
open class InteractiveStoryBaseEvent(
id: HexKey,
@@ -47,75 +36,11 @@ open class InteractiveStoryBaseEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = tags.firstTagValue("title")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun summary() = tags.firstTagValue("summary")
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
fun image() = tags.firstTagValue("image")
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun options() =
tags
.filter { it.size > 2 && it[0] == "option" }
.mapNotNull { ATag.parse(it[2], it.getOrNull(3))?.let { aTag -> StoryOption(it[1], aTag) } }
companion object {
fun generalTags(
content: String,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
): Array<Array<String>> {
val tags = mutableListOf<Array<String>>()
tags.addAll(buildHashtagTags(findHashtags(content)))
tags.addAll(buildUrlRefs(findURLs(content)))
zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) }
zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) }
if (markAsSensitive) {
tags.add(ContentWarningSerializer.toTagArray())
}
geohash?.let { tags.addAll(geohashMipMap(it)) }
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
return tags.toTypedArray()
}
fun makeTags(
baseId: String,
alt: String,
title: String,
summary: String? = null,
image: String? = null,
options: List<StoryOption> = emptyList(),
): Array<Array<String>> =
(
listOfNotNull(
arrayOf("d", baseId),
arrayOf("title", title),
summary?.let { arrayOf("summary", it) },
image?.let { arrayOf("image", it) },
AltTagSerializer.toTagArray(alt),
) +
options.map {
val relayUrl = it.address.relay
if (relayUrl != null) {
arrayOf("option", it.option, it.address.toTag(), relayUrl)
} else {
arrayOf("option", it.option, it.address.toTag())
}
}
).toTypedArray()
}
fun options() = tags.mapNotNull(StoryOptionTag::parse)
}
class StoryOption(
val option: String,
val address: ATag,
)
@@ -20,12 +20,14 @@
*/
package com.vitorpamplona.quartz.experimental.interactiveStories
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.utils.TimeUtils
class InteractiveStoryPrologueEvent(
@@ -39,7 +41,7 @@ class InteractiveStoryPrologueEvent(
RootScope {
companion object {
const val KIND = 30296
const val ALT = "The prologue of an interative story called "
const val ALT = "The prologue of an interactive story called "
fun createAddressATag(
pubKey: HexKey,
@@ -51,32 +53,19 @@ class InteractiveStoryPrologueEvent(
dtag: String,
): String = ATag.assembleATagId(KIND, pubKey, dtag)
fun create(
fun build(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
summary: String? = null,
image: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
signer: NostrSigner,
options: List<StoryOptionTag>,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (InteractiveStoryPrologueEvent) -> Unit,
) {
val tags =
makeTags(baseId, ALT + title, title, summary, image, options) +
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, imetas)
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
} else {
signer.sign(createdAt, KIND, tags, content, onReady)
initializer: TagArrayBuilder<InteractiveStoryPrologueEvent>.() -> Unit = {},
): EventTemplate<InteractiveStoryPrologueEvent> =
eventTemplate(KIND, content, createdAt) {
dTag(baseId)
title(title)
options(options)
initializer()
}
}
}
}
@@ -21,15 +21,22 @@
package com.vitorpamplona.quartz.experimental.interactiveStories
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.ReadStatusTag
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.RootSceneTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.firstTag
import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.builder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag
import com.vitorpamplona.quartz.nip99Classifieds.tags.StatusTag
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
@Immutable
class InteractiveStoryReadingStateEvent(
@@ -40,17 +47,17 @@ class InteractiveStoryReadingStateEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun title() = tags.firstTagValue("title")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun summary() = tags.firstTagValue("summary")
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
fun image() = tags.firstTagValue("image")
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun status() = tags.firstTagValue("status")
fun status() = tags.firstNotNullOfOrNull(StatusTag::parse)
fun root() = tags.firstTag("A")?.let { ATag.parse(it[1], it.getOrNull(2)) }
fun root() = tags.firstNotNullOfOrNull(RootSceneTag::parse)
fun currentScene() = tags.firstTag("a")?.let { ATag.parse(it[1], it.getOrNull(2)) }
fun currentScene() = tags.firstNotNullOfOrNull(ATag::parse)
companion object {
const val KIND = 30298
@@ -71,71 +78,60 @@ class InteractiveStoryReadingStateEvent(
base: InteractiveStoryReadingStateEvent,
currentScene: InteractiveStoryBaseEvent,
currentSceneRelay: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
) {
): EventTemplate<InteractiveStoryReadingStateEvent> {
val rootTag = base.dTag()
val sceneTag = currentScene.addressTag()
val sceneTag = currentScene.aTag(currentSceneRelay)
val status =
if (rootTag == sceneTag) {
"new"
if (rootTag == sceneTag.toTag()) {
ReadStatusTag.STATUS.NEW
} else if (currentScene.options().isEmpty()) {
"done"
ReadStatusTag.STATUS.DONE
} else {
"reading"
ReadStatusTag.STATUS.READING
}
val tags =
base.tags.filter { it[0] != "a" && it[0] != "status" } +
listOf(
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
arrayOf("status", status),
)
val updatedTags =
base.tags.builder {
currentScene(sceneTag)
status(status)
}
signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady)
return EventTemplate(createdAt, KIND, updatedTags, "")
}
fun create(
fun build(
root: InteractiveStoryBaseEvent,
rootRelay: String?,
currentScene: InteractiveStoryBaseEvent,
currentSceneRelay: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
) {
val rootTag = root.addressTag()
val sceneTag = currentScene.addressTag()
initializer: TagArrayBuilder<InteractiveStoryReadingStateEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
val rootTag = root.aTag(rootRelay)
val sceneTag = currentScene.aTag(currentSceneRelay)
val status =
if (rootTag == sceneTag) {
"new"
ReadStatusTag.STATUS.NEW
} else if (currentScene.options().isEmpty()) {
"done"
ReadStatusTag.STATUS.DONE
} else {
"reading"
ReadStatusTag.STATUS.READING
}
val tags =
listOfNotNull(
arrayOf("d", rootTag),
AltTagSerializer.toTagArray(root.title()?.let { ALT2 + it } ?: ALT1),
root.title()?.let { arrayOf("title", it) },
root.summary()?.let { arrayOf("summary", it) },
root.image()?.let { arrayOf("image", it) },
removeTrailingNullsAndEmptyOthers("A", rootTag, rootRelay),
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
arrayOf("status", status),
).toTypedArray()
dTag(rootTag.toTag())
alt(root.title()?.let { ALT2 + it } ?: ALT1)
signer.sign(createdAt, KIND, tags, "", onReady)
rootScene(rootTag)
currentScene(sceneTag)
status(status)
root.title()?.let { storyTitle(it) }
root.summary()?.let { storyImage(it) }
root.image()?.let { storySummary(it) }
initializer()
}
}
enum class ReadingStatus {
NEW,
READING,
DONE,
}
}
@@ -20,12 +20,13 @@
*/
package com.vitorpamplona.quartz.experimental.interactiveStories
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.utils.TimeUtils
class InteractiveStorySceneEvent(
@@ -51,30 +52,18 @@ class InteractiveStorySceneEvent(
dtag: String,
): String = ATag.assembleATagId(KIND, pubKey, dtag)
fun create(
fun build(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
signer: NostrSigner,
options: List<StoryOptionTag>,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (InteractiveStorySceneEvent) -> Unit,
) {
val tags =
makeTags(baseId, ALT + title, title, options = options) +
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, imetas)
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
} else {
signer.sign(createdAt, KIND, tags, content, onReady)
}
initializer: TagArrayBuilder<InteractiveStorySceneEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
dTag(baseId)
title(title)
options(options)
initializer()
}
}
}
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2024 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.interactiveStories
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.ReadStatusTag
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
fun <T : InteractiveStoryBaseEvent> TagArrayBuilder<T>.title(title: String) = addUnique(TitleTag.assemble(title))
fun <T : InteractiveStoryBaseEvent> TagArrayBuilder<T>.option(option: StoryOptionTag) = add(option.toTagArray())
fun <T : InteractiveStoryBaseEvent> TagArrayBuilder<T>.options(options: List<StoryOptionTag>) = addAll(options.map { it.toTagArray() })
fun <T : InteractiveStoryBaseEvent> TagArrayBuilder<T>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt))
fun TagArrayBuilder<InteractiveStoryPrologueEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary))
fun TagArrayBuilder<InteractiveStoryPrologueEvent>.image(imageUrl: String) = add(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<InteractiveStoryPrologueEvent>.images(imageUrls: List<String>) = addAll(imageUrls.map { ImageTag.assemble(it) })
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.storyTitle(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.storySummary(summary: String) = addUnique(SummaryTag.assemble(summary))
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.storyImage(imageUrl: String) = add(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.storyImages(imageUrls: List<String>) = addAll(imageUrls.map { ImageTag.assemble(it) })
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.rootScene(scene: ATag) = addUnique(scene.toATagArray())
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.currentScene(scene: ATag) = addUnique(scene.toATagArray())
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.status(status: ReadStatusTag.STATUS) = addUnique(status.toTagArray())
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2024 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.interactiveStories.tags
class ReadStatusTag {
enum class STATUS(
val value: String,
) {
NEW("new"),
READING("reading"),
DONE("done"),
;
fun toTagArray() = assemble(this)
}
companion object {
const val TAG_NAME = "status"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(status: STATUS) = arrayOf(TAG_NAME, status.value)
}
}
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2024 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.interactiveStories.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
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.pointerSizeInBytes
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
@Immutable
data class RootSceneTag(
val kind: Int,
val pubKeyHex: String,
val dTag: String,
) {
var relay: String? = null
constructor(
kind: Int,
pubKeyHex: HexKey,
dTag: String,
relayHint: String?,
) : this(kind, pubKeyHex, dTag) {
this.relay = relayHint
}
fun countMemory(): Long =
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
8L + // kind
pubKeyHex.bytesUsedInMemory() +
dTag.bytesUsedInMemory() +
(relay?.bytesUsedInMemory() ?: 0)
fun toTag() = assembleATagId(kind, pubKeyHex, dTag)
fun toTagArray() = removeTrailingNullsAndEmptyOthers(TAG_NAME, toTag(), relay)
companion object {
const val TAG_NAME = "A"
const val TAG_SIZE = 2
fun assembleATagId(
kind: Int,
pubKeyHex: HexKey,
dTag: String,
) = Address.assemble(kind, pubKeyHex, dTag)
fun parse(
aTagId: String,
relay: String?,
) = Address.parse(aTagId)?.let { RootSceneTag(it.kind, it.pubKeyHex, it.dTag, relay) }
@JvmStatic
fun parse(tag: Array<String>): RootSceneTag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return parse(tag[1], tag.getOrNull(2))
}
@JvmStatic
fun assemble(
aTagId: HexKey,
relay: String?,
) = arrayOfNotNull(TAG_NAME, aTagId, relay)
@JvmStatic
fun assemble(
kind: Int,
pubKeyHex: String,
dTag: String,
relay: String?,
) = arrayOfNotNull(TAG_NAME, assembleATagId(kind, pubKeyHex, dTag), relay)
}
}
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2024 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.interactiveStories.tags
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.utils.arrayOfNotNull
class StoryOptionTag(
val option: String,
val address: ATag,
) {
fun toTagArray() = assemble(option, address)
companion object {
const val TAG_NAME = "option"
const val TAG_SIZE = 3
@JvmStatic
fun parse(tag: Array<String>): StoryOptionTag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return ATag.parse(tag[2], tag.getOrNull(3))?.let { StoryOptionTag(tag[1], it) }
}
@JvmStatic
fun assemble(
title: String,
destination: ATag,
) = arrayOfNotNull(TAG_NAME, title, destination.toTag(), destination.relay)
}
}
@@ -21,7 +21,7 @@
package com.vitorpamplona.quartz.experimental.limits
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip13Pow.pow
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -21,9 +21,12 @@
package com.vitorpamplona.quartz.experimental.medical
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent.Companion.ALT
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -37,16 +40,15 @@ class FhirResourceEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 82
const val ALT_DESCRIPTION = "Medical data"
fun create(
fun build(
fhirPayload: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FhirResourceEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
signer.sign(createdAt, KIND, tags.toTypedArray(), fhirPayload, onReady)
initializer: TagArrayBuilder<FhirResourceEvent>.() -> Unit = {},
) = eventTemplate(KIND, fhirPayload, createdAt) {
alt(ALT)
initializer()
}
}
}
@@ -1,115 +0,0 @@
/**
* Copyright (c) 2024 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.nip95
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class FileStorageHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun dataEventId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE && mimeTypes.contains(it[1]) }
companion object {
const val KIND = 1065
const val ALT_DESCRIPTION = "Descriptors for a binary file"
private const val ENCRYPTION_KEY = "aes-256-gcm"
private const val MIME_TYPE = "m"
private const val FILE_SIZE = "size"
private const val DIMENSION = "dim"
private const val HASH = "x"
private const val MAGNET_URI = "magnet"
private const val TORRENT_INFOHASH = "i"
private const val BLUR_HASH = "blurhash"
private const val ALT = "alt"
fun create(
storageEvent: FileStorageEvent,
mimeType: String? = null,
alt: String? = null,
hash: String? = null,
size: String? = null,
dimensions: Dimension? = null,
blurhash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
sensitiveContent: Boolean? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FileStorageHeaderEvent) -> Unit,
) {
val tags =
listOfNotNull(
arrayOf("e", storageEvent.id),
mimeType?.let { arrayOf(MIME_TYPE, mimeType) },
hash?.let { arrayOf(HASH, it) },
alt?.let { arrayOf(ALT, it) } ?: AltTagSerializer.toTagArray(ALT_DESCRIPTION),
size?.let { arrayOf(FILE_SIZE, it) },
dimensions?.let { arrayOf(DIMENSION, it.toString()) },
blurhash?.let { arrayOf(BLUR_HASH, it) },
magnetURI?.let { arrayOf(MAGNET_URI, it) },
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
sensitiveContent?.let {
if (it) {
ContentWarningSerializer.toTagArray()
} else {
null
}
},
)
val content = alt ?: ""
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
}
}
}
@@ -18,14 +18,15 @@
* 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.nip95
package com.vitorpamplona.quartz.experimental.nip95.data
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.Base64
@@ -40,40 +41,26 @@ class FileStorageEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
override fun isContentEncoded() = true
fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
fun decode(): ByteArray? =
try {
Base64.getDecoder().decode(content)
} catch (e: Exception) {
Log.e("FileStorageEvent", "Unable to decode base 64 ${e.message} $content")
null
}
fun decode(): ByteArray? = decode(content)
companion object {
const val KIND = 1064
const val ALT = "Binary data"
private const val TYPE = "type"
private const val DECRYPT = "decrypt"
fun decode(content: String): ByteArray? = runCatching { Base64.getDecoder().decode(content) }.getOrNull()
fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
fun create(
mimeType: String,
fun build(
data: ByteArray,
signer: NostrSigner,
mimeType: String? = null,
createdAt: Long = TimeUtils.now(),
onReady: (FileStorageEvent) -> Unit,
) {
val tags =
listOfNotNull(
arrayOf(TYPE, mimeType),
AltTagSerializer.toTagArray(ALT),
)
val content = encode(data)
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
}
initializer: TagArrayBuilder<FileStorageEvent>.() -> Unit = {},
): EventTemplate<FileStorageEvent> =
eventTemplate(KIND, encode(data), createdAt) {
alt(ALT)
mimeType?.let { mimeType(it) }
initializer()
}
}
}
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2024 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.nip95.data
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
fun TagArrayBuilder<FileStorageEvent>.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType))
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2024 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.nip95.header
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.nip01Core.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.any
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.eTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class FileStorageHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun dataEvent() = tags.mapNotNull(ETag::parse)
fun dataEventIds() = tags.mapNotNull(ETag::parseId)
fun dataEventId() = tags.firstNotNullOfOrNull(ETag::parseId)
fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse)
fun hash() = tags.firstNotNullOfOrNull(HashTag::parse)
fun size() = tags.firstNotNullOfOrNull(SizeTag::parse)
fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse)
fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse)
fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse)
fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse)
fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse)
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
fun fallback() = tags.firstNotNullOfOrNull(FallbackTag::parse)
fun isOneOf(mimeTypes: Set<String>) = tags.any(MimeTypeTag::isIn, mimeTypes)
companion object {
const val KIND = 1065
const val ALT_DESCRIPTION = "Descriptors for a binary file"
fun build(
storageEvent: EventHintBundle<FileStorageEvent>,
caption: String?,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<FileStorageHeaderEvent>.() -> Unit = {},
) = eventTemplate(KIND, caption ?: "", createdAt) {
eTag(storageEvent.toETag())
caption?.ifBlank { null }?.let { alt(caption) } ?: alt(ALT_DESCRIPTION)
initializer()
}
}
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2024 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.nip95.header
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash
fun TagArrayBuilder<FileStorageHeaderEvent>.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType))
fun TagArrayBuilder<FileStorageHeaderEvent>.hash(hash: HexKey) = add(HashTag.assemble(hash))
fun TagArrayBuilder<FileStorageHeaderEvent>.fileSize(size: Int) = add(SizeTag.assemble(size))
fun TagArrayBuilder<FileStorageHeaderEvent>.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim))
fun TagArrayBuilder<FileStorageHeaderEvent>.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash))
fun TagArrayBuilder<FileStorageHeaderEvent>.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash))
fun TagArrayBuilder<FileStorageHeaderEvent>.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri))
fun TagArrayBuilder<FileStorageHeaderEvent>.image(imageUrl: HexKey) = add(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<FileStorageHeaderEvent>.thumb(trumbUrl: HexKey) = add(ThumbTag.assemble(trumbUrl))
fun TagArrayBuilder<FileStorageHeaderEvent>.summary(summary: HexKey) = add(SummaryTag.assemble(summary))
fun TagArrayBuilder<FileStorageHeaderEvent>.fallback(fallbackUrl: HexKey) = add(FallbackTag.assemble(fallbackUrl))
fun TagArrayBuilder<FileStorageHeaderEvent>.service(service: HexKey) = add(ServiceTag.assemble(service))
@@ -21,10 +21,14 @@
package com.vitorpamplona.quartz.experimental.nns
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.nns.tags.IPv4Tag
import com.vitorpamplona.quartz.experimental.nns.tags.IPv6Tag
import com.vitorpamplona.quartz.experimental.nns.tags.VersionTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -36,23 +40,32 @@ class NNSEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun ip4() = tags.firstOrNull { it.size > 1 && it[0] == "ip4" }?.get(1)
fun ip4() = tags.firstNotNullOfOrNull(IPv4Tag::parse)
fun ip6() = tags.firstOrNull { it.size > 1 && it[0] == "ip6" }?.get(1)
fun ip6() = tags.firstNotNullOfOrNull(IPv6Tag::parse)
fun version() = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.get(1)
fun version() = tags.firstNotNullOfOrNull(VersionTag::parse)
companion object {
const val KIND = 30053
const val ALT = "DNS records"
fun create(
signer: NostrSigner,
fun build(
createdAt: Long = TimeUtils.now(),
onReady: (NNSEvent) -> Unit,
) {
val tags = arrayOf(AltTagSerializer.toTagArray(ALT))
signer.sign(createdAt, KIND, tags, "", onReady)
initializer: TagArrayBuilder<NNSEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT)
initializer()
}
fun build(
ipv4: String,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<NNSEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT)
ipv4(ipv4)
initializer()
}
}
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2024 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.nns
import com.vitorpamplona.quartz.experimental.nns.tags.IPv4Tag
import com.vitorpamplona.quartz.experimental.nns.tags.IPv6Tag
import com.vitorpamplona.quartz.experimental.nns.tags.VersionTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<NNSEvent>.ipv4(ip: String) = add(IPv4Tag.assemble(ip))
fun TagArrayBuilder<NNSEvent>.ipv6(ip: String) = add(IPv6Tag.assemble(ip))
fun TagArrayBuilder<NNSEvent>.version(version: String) = add(VersionTag.assemble(version))
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.nns.tags
class IPv4Tag {
companion object {
const val TAG_NAME = "ip4"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(ip: String) = arrayOf(TAG_NAME, ip)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.nns.tags
class IPv6Tag {
companion object {
const val TAG_NAME = "ip6"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(ip: String) = arrayOf(TAG_NAME, ip)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.nns.tags
class VersionTag {
companion object {
const val TAG_NAME = "version"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(ip: String) = arrayOf(TAG_NAME, ip)
}
}
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -142,7 +142,7 @@ class GalleryListEvent(
if (tags.any { it.size > 1 && it[0] == "alt" }) {
tags
} else {
tags + AltTagSerializer.toTagArray(ALT)
tags + AltTag.assemble(ALT)
}
signer.sign(createdAt, KIND, newTags, content, onReady)
@@ -23,10 +23,24 @@ package com.vitorpamplona.quartz.experimental.profileGallery
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.any
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash
import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -38,97 +52,54 @@ class ProfileGalleryEntryEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
fun url() = tags.firstNotNullOfOrNull(UrlTag::parse)
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
fun urls() = tags.mapNotNull(UrlTag::parse)
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse)
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
fun hash() = tags.firstNotNullOfOrNull(HashTag::parse)
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
fun size() = tags.firstNotNullOfOrNull(SizeTag::parse)
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse)
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse)
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse)
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse)
fun fromEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse)
fun hasFromEvent() = tags.any { it.size > 1 && it[0] == "e" }
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) }
fun fallback() = tags.firstNotNullOfOrNull(FallbackTag::parse)
fun hasUrl() = tags.any(UrlTag::isTag)
fun isOneOf(mimeTypes: Set<String>) = tags.any(MimeTypeTag::isIn, mimeTypes)
fun fromEvent() = tags.firstNotNullOfOrNull(ETag::parseId)
fun hasFromEvent() = tags.any(ETag::isTagged)
companion object {
const val KIND = 1163
const val ALT_DESCRIPTION = "Profile Gallery Entry"
const val URL = "url"
const val ENCRYPTION_KEY = "aes-256-gcm"
const val MIME_TYPE = "m"
const val FILE_SIZE = "size"
const val DIMENSION = "dim"
const val HASH = "x"
const val MAGNET_URI = "magnet"
const val TORRENT_INFOHASH = "i"
const val BLUR_HASH = "blurhash"
const val ORIGINAL_HASH = "ox"
const val ALT = "alt"
fun create(
fun build(
url: String,
eventid: String? = null,
relayhint: String? = null,
magnetUri: String? = null,
mimeType: String? = null,
alt: String? = null,
hash: String? = null,
size: String? = null,
dimensions: Dimension? = null,
blurhash: String? = null,
originalHash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
sensitiveContent: Boolean? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ProfileGalleryEntryEvent) -> Unit,
) {
var etag = eventid?.let { arrayOf("e", it) }
relayhint?.let { etag = etag?.plus(it) }
val tags =
listOfNotNull(
arrayOf(URL, url),
eventid?.let { etag },
magnetUri?.let { arrayOf(MAGNET_URI, it) },
mimeType?.let { arrayOf(MIME_TYPE, it) },
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: AltTagSerializer.toTagArray(ALT_DESCRIPTION),
hash?.let { arrayOf(HASH, it) },
size?.let { arrayOf(FILE_SIZE, it) },
dimensions?.let { arrayOf(DIMENSION, it.toString()) },
blurhash?.let { arrayOf(BLUR_HASH, it) },
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
magnetURI?.let { arrayOf(MAGNET_URI, it) },
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
sensitiveContent?.let {
if (it) {
ContentWarningSerializer.toTagArray()
} else {
null
}
},
)
val content = alt ?: ""
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
initializer: TagArrayBuilder<ProfileGalleryEntryEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
url(url)
alt(ALT_DESCRIPTION)
initializer()
}
}
}
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2024 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.profileGallery
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.OriginalHashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.SummaryTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash
import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag
fun TagArrayBuilder<ProfileGalleryEntryEvent>.url(url: String) = add(UrlTag.assemble(url))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.hash(hash: HexKey) = add(HashTag.assemble(hash))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.fileSize(size: Int) = add(SizeTag.assemble(size))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.originalHash(hash: HexKey) = add(OriginalHashTag.assemble(hash))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.image(imageUrl: String) = add(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.thumb(trumbUrl: String) = add(ThumbTag.assemble(trumbUrl))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.summary(summary: String) = add(SummaryTag.assemble(summary))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.fallback(fallbackUrl: String) = add(FallbackTag.assemble(fallbackUrl))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.service(service: String) = add(ServiceTag.assemble(service))
fun TagArrayBuilder<ProfileGalleryEntryEvent>.fromEvent(
event: HexKey,
relayHint: String?,
) = add(ETag.assemble(event, relayHint, null))
@@ -21,9 +21,16 @@
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.RankTag
import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.tagArray
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip51Lists.Nip51PrivateTags
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -36,48 +43,42 @@ class RelationshipStatusEvent(
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun rank() = tags.firstNotNullOfOrNull(RankTag::parse)
fun petname() = tags.firstNotNullOfOrNull(PetnameTag::parse)
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
companion object {
const val KIND = 30382
const val ALT = "Relationship Status"
const val PETNAME = "petname"
const val SUMMARY = "summary"
private fun create(
content: String,
tags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (RelationshipStatusEvent) -> Unit,
) {
val newTags =
if (tags.any { it.size > 1 && it[0] == "alt" }) {
tags
} else {
tags + AltTagSerializer.toTagArray(ALT)
}
signer.sign(createdAt, KIND, newTags, content, onReady)
}
fun create(
targetUser: HexKey,
petname: String? = null,
summary: String? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
publicInitializer: TagArrayBuilder<RelationshipStatusEvent>.() -> Unit = {},
privateInitializer: TagArrayBuilder<RelationshipStatusEvent>.() -> Unit = {},
onReady: (RelationshipStatusEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("d", targetUser))
tags.add(AltTagSerializer.toTagArray(ALT))
val publicTags =
tagArray {
alt(ALT)
dTag(targetUser)
publicInitializer()
}
val privateTags = mutableListOf<Array<String>>()
petname?.let { privateTags.add(arrayOf(PETNAME, it)) }
summary?.let { privateTags.add(arrayOf(SUMMARY, it)) }
val privateTags =
tagArray {
petname?.let { petname(it) }
summary?.let { summary(it) }
privateInitializer()
}
encryptTags(privateTags.toTypedArray(), signer) { content ->
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
Nip51PrivateTags.encryptNip44(privateTags, signer) { content ->
signer.sign(createdAt, KIND, publicTags, content, onReady)
}
}
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2024 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.relationshipStatus
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>.summary(summary: String) = add(SummaryTag.assemble(summary))
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.relationshipStatus.tags
class PetnameTag {
companion object {
const val TAG_NAME = "petname"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(petname: String) = arrayOf(TAG_NAME, petname)
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.relationshipStatus.tags
class RankTag {
companion object {
const val TAG_NAME = "rank"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): Int? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1].toIntOrNull()
}
@JvmStatic
fun assemble(rank: Int) = arrayOf(TAG_NAME, rank.toString())
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.relationshipStatus.tags
class SummaryTag {
companion object {
const val TAG_NAME = "summary"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1]
}
@JvmStatic
fun assemble(ip: String) = arrayOf(TAG_NAME, ip)
}
}
@@ -21,132 +21,54 @@
package com.vitorpamplona.quartz.experimental.zapPolls
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.zapPolls.tags.ClosedAtTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.ConsensusThresholdTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.MaximumTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.MinimumTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashMipMap
import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrl
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningSerializer
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupSerializer
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.ZapRaiserSerializer
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
const val POLL_OPTION = "poll_option"
const val VALUE_MAXIMUM = "value_maximum"
const val VALUE_MINIMUM = "value_minimum"
const val CONSENSUS_THRESHOLD = "consensus_threshold"
const val CLOSED_AT = "closed_at"
@Immutable
class PollNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
// ots: String?, TODO implement OTS: https://github.com/opentimestamps/java-opentimestamps
content: String,
sig: HexKey,
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun pollOptions() = tags.filter { it.size > 2 && it[0] == POLL_OPTION }.associate { it[1].toInt() to it[2] }
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun pollOptionsArray() = tags.mapNotNull(PollOptionTag::parse)
fun minimumAmount() = tags.firstOrNull { it.size > 1 && it[0] == VALUE_MINIMUM }?.getOrNull(1)?.toLongOrNull()
fun pollOptions() = pollOptionsArray().associate { it.index to it.descriptor }
fun maximumAmount() = tags.firstOrNull { it.size > 1 && it[0] == VALUE_MAXIMUM }?.getOrNull(1)?.toLongOrNull()
fun minAmount() = tags.firstNotNullOfOrNull(MinimumTag::parse)
fun getTagLong(property: String): Long? {
val number = tags.firstOrNull { it.size > 1 && it[0] == property }?.get(1)
fun maxAmount() = tags.firstNotNullOfOrNull(MaximumTag::parse)
return if (number.isNullOrBlank() || number == "null") {
null
} else {
number.toLong()
}
}
fun closedAt() = tags.firstNotNullOfOrNull(ClosedAtTag::parse)
fun consensusThreshold() = tags.firstNotNullOfOrNull(ConsensusThresholdTag::parse)
companion object {
const val KIND = 6969
const val ALT = "Poll event"
const val ALT_DESCRIPTION = "Poll event"
fun create(
msg: String,
replyTos: List<String>?,
mentions: List<String>?,
addresses: List<ATag>?,
signer: NostrSigner,
fun build(
post: String,
options: List<PollOptionTag>,
createdAt: Long = TimeUtils.now(),
pollOptions: Map<Int, String>,
valueMaximum: Int?,
valueMinimum: Int?,
consensusThreshold: Int?,
closedAt: Int?,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean,
zapRaiserAmount: Long?,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean,
onReady: (PollNoteEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
replyTos?.forEach { tags.add(arrayOf("e", it)) }
mentions?.forEach { tags.add(arrayOf("p", it)) }
addresses?.forEach { tags.add(arrayOf("a", it.toTag())) }
pollOptions.forEach { poll_op ->
tags.add(arrayOf(POLL_OPTION, poll_op.key.toString(), poll_op.value))
}
valueMaximum?.let { tags.add(arrayOf(VALUE_MAXIMUM, valueMaximum.toString())) }
valueMinimum?.let { tags.add(arrayOf(VALUE_MINIMUM, valueMinimum.toString())) }
consensusThreshold?.let {
tags.add(arrayOf(CONSENSUS_THRESHOLD, consensusThreshold.toString()))
}
closedAt?.let { tags.add(arrayOf(CLOSED_AT, closedAt.toString())) }
zapReceiver?.forEach { tags.add(ZapSplitSetupSerializer.toTagArray(it)) }
zapRaiserAmount?.let { tags.add(ZapRaiserSerializer.toTagArray(it)) }
if (markAsSensitive) {
tags.add(ContentWarningSerializer.toTagArray())
}
geohash?.let { tags.addAll(geohashMipMap(it)) }
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(AltTagSerializer.toTagArray(ALT))
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
} else {
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
}
initializer: TagArrayBuilder<PollNoteEvent>.() -> Unit = {},
): EventTemplate<PollNoteEvent> {
val tags = TagArrayBuilder<PollNoteEvent>()
tags.pollOptions(options)
tags.alt(ALT_DESCRIPTION)
tags.apply(initializer)
return EventTemplate(createdAt, KIND, tags.build(), post)
}
}
}
/*
{
"id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <unix timestamp in seconds>,
"kind": 6969,
"tags": [
["e", <32-bytes hex of the id of the poll event>, <primary poll host relay URL>],
["p", <32-bytes hex of the key>, <primary poll host relay URL>],
["poll_option", "0", "poll option 0 description string"],
["poll_option", "1", "poll option 1 description string"],
["poll_option", "n", "poll option <n> description string"],
["value_maximum", "maximum satoshi value for inclusion in tally"],
["value_minimum", "minimum satoshi value for inclusion in tally"],
["consensus_threshold", "required percentage to attain consensus <0..100>"],
["closed_at", "unix timestamp in seconds"],
],
"ots": <base64-encoded OTS file data>
"content": <primary poll description string>,
"sig": <64-bytes hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field>
}
*/
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2024 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.zapPolls
import com.vitorpamplona.quartz.experimental.zapPolls.tags.ClosedAtTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.ConsensusThresholdTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.MaximumTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.MinimumTag
import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<PollNoteEvent>.consensusThreshold(percentage: Double) = addUnique(ConsensusThresholdTag.assemble(percentage))
fun TagArrayBuilder<PollNoteEvent>.minAmount(value: Long) = addUnique(MinimumTag.assemble(value))
fun TagArrayBuilder<PollNoteEvent>.maxAmount(value: Long) = addUnique(MaximumTag.assemble(value))
fun TagArrayBuilder<PollNoteEvent>.closedAt(timestamp: Long) = addUnique(ClosedAtTag.assemble(timestamp))
fun TagArrayBuilder<PollNoteEvent>.pollOption(
index: Int,
description: String,
) = add(PollOptionTag.assemble(index, description))
fun TagArrayBuilder<PollNoteEvent>.pollOptions(options: Map<Int, String>) = addAll(options.map { PollOptionTag.assemble(it.key, it.value) })
fun TagArrayBuilder<PollNoteEvent>.pollOptions(options: List<PollOptionTag>) = addAll(options.map { it.toTagArray() })
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.zapPolls.tags
class ClosedAtTag {
companion object {
const val TAG_NAME = "closed_at"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): Long? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1].toLongOrNull()
}
@JvmStatic
fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString())
}
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2024 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.zapPolls.tags
import kotlin.math.round
class ConsensusThresholdTag {
companion object {
const val TAG_NAME = "consensus_threshold"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): Double? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1].toLongOrNull()?.toDouble()?.div(100)
}
@JvmStatic
fun assemble(percentage: Double) = arrayOf(TAG_NAME, (round(percentage * 100)).toString())
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.zapPolls.tags
class MaximumTag {
companion object {
const val TAG_NAME = "value_maximum"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): Long? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1].toLongOrNull()
}
@JvmStatic
fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString())
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2024 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.zapPolls.tags
class MinimumTag {
companion object {
const val TAG_NAME = "value_minimum"
const val TAG_SIZE = 2
@JvmStatic
fun parse(tag: Array<String>): Long? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
return tag[1].toLongOrNull()
}
@JvmStatic
fun assemble(timestamp: Long) = arrayOf(TAG_NAME, timestamp.toString())
}
}
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2024 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.zapPolls.tags
class PollOptionTag(
val index: Int,
val descriptor: String,
) {
fun toTagArray() = assemble(index, descriptor)
companion object {
const val TAG_NAME = "poll_option"
const val TAG_SIZE = 3
@JvmStatic
fun parse(tag: Array<String>): PollOptionTag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
val index = tag[1].toIntOrNull() ?: return null
return PollOptionTag(index, tag[2])
}
@JvmStatic
fun assemble(
index: Int,
descriptor: String,
) = arrayOf(TAG_NAME, index.toString(), descriptor)
}
}
@@ -27,19 +27,19 @@ import com.vitorpamplona.quartz.utils.Hex
fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tags, content)
fun Event.hasCorrectIDHash(): Boolean {
fun Event.verifyId(): Boolean {
if (id.isEmpty()) return false
return id == generateId()
}
fun Event.hasVerifiedSignature(): Boolean {
fun Event.verifySignature(): Boolean {
if (id.isEmpty() || sig.isEmpty()) return false
return CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
}
/** Checks if the ID is correct and then if the pubKey's secret key signed the event. */
fun Event.checkSignature() {
if (!hasCorrectIDHash()) {
if (!verifyId()) {
throw Exception(
"""
|Unexpected ID.
@@ -49,14 +49,14 @@ fun Event.checkSignature() {
""".trimIndent(),
)
}
if (!hasVerifiedSignature()) {
if (!verifySignature()) {
throw Exception("""Bad signature!""")
}
}
fun Event.hasValidSignature(): Boolean =
fun Event.verify(): Boolean =
try {
hasCorrectIDHash() && hasVerifiedSignature()
verifyId() && verifySignature()
} catch (e: Exception) {
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
false
@@ -22,6 +22,11 @@ package com.vitorpamplona.quartz.nip01Core
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
@@ -31,9 +36,11 @@ data class EventHintBundle<T : Event>(
val event: T,
) {
var relay: String? = null
var authorHomeRelay: String? = null
constructor(event: T, relayHint: String? = null) : this(event) {
constructor(event: T, relayHint: String? = null, authorHomeRelay: String? = null) : this(event) {
this.relay = relayHint
this.authorHomeRelay = authorHomeRelay
}
fun countMemory(): Long =
@@ -43,9 +50,15 @@ data class EventHintBundle<T : Event>(
fun toNEvent(): String = NEvent.create(event.id, event.pubKey, event.kind, relay)
fun toTagArray(tag: String) = listOfNotNull(tag, event.id, relay, event.pubKey).toTypedArray()
fun toETag() = ETag(event.id, relay, event.pubKey)
fun toETagArray() = toTagArray("e")
fun toATag() = ATag(event.kind, event.pubKey, event.dTag(), relay)
fun toQTagArray() = toTagArray("q")
fun toPTag() = PTag(event.pubKey, authorHomeRelay)
fun toMarkedETag(marker: MarkedETag.MARKER) = MarkedETag(event.id, relay, marker, event.pubKey)
fun toETagArray() = ETag.assemble(event.id, relay, event.pubKey)
fun toQTagArray() = ETag(event.id, relay, event.pubKey).toQTagArray()
}
@@ -22,12 +22,15 @@ package com.vitorpamplona.quartz.nip01Core.core
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
@Immutable
interface AddressableEvent {
interface AddressableEvent : IEvent {
fun dTag(): String
fun address(relayHint: String? = null): ATag
fun aTag(relayHint: String? = null): ATag
fun address(): Address
fun addressTag(): String
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.core
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
@Immutable
@@ -38,7 +39,9 @@ open class BaseAddressableEvent(
AddressableEvent {
override fun dTag() = tags.dTag()
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
override fun address() = Address(kind, pubKey, dTag())
/**
* Creates the tag in a memory efficient way (without creating the ATag class
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.core
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
@Immutable
open class BaseReplaceableEvent(
@@ -36,7 +37,9 @@ open class BaseReplaceableEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
override fun dTag() = FIXED_D_TAG
override fun address(relayHint: String?) = ATag(kind, pubKey, FIXED_D_TAG, relayHint)
override fun aTag(relayHint: String?) = ATag(kind, pubKey, FIXED_D_TAG, relayHint)
override fun address() = Address(kind, pubKey, dTag())
/**
* Creates the tag in a memory efficient way (without creating the ATag class
@@ -39,7 +39,7 @@ open class Event(
val tags: TagArray,
val content: String,
val sig: HexKey,
) {
) : IEvent {
open fun isContentEncoded() = false
open fun countMemory(): Long =
@@ -18,10 +18,6 @@
* 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.nip99Classifieds
package com.vitorpamplona.quartz.nip01Core.core
data class Price(
val amount: String,
val currency: String?,
val frequency: String?,
)
interface IEvent
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2024 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.core
typealias Tag = Array<String>
fun Tag.name() = this[0]
fun Tag.value() = this[1]
fun Tag.hasValue() = this[1].isNotEmpty()
fun Tag.nameOrNull() = if (size > 0) name() else null
fun Tag.valueOrNull() = if (size > 1) value() else null
fun Tag.isNameUnsafe(name: String): Boolean = name() == name
fun Tag.isValueUnsafe(value: String): Boolean = value() == value
fun Tag.isValueInUnsafe(values: Set<String>): Boolean = value() in values
fun Tag.match(name: String): Boolean = if (size > 0) isNameUnsafe(name) else false
fun Tag.isValue(value: String): Boolean = if (size > 1) isValueUnsafe(value) else false
fun Tag.match(
name: String,
value: String,
minSize: Int,
): Boolean = if (size >= minSize) isNameUnsafe(name) && isValueUnsafe(value) else false
fun Tag.match(
name: String,
values: Set<String>,
minSize: Int,
): Boolean = if (size >= minSize) isNameUnsafe(name) && isValueInUnsafe(values) else false
fun Tag.match(
name: String,
minSize: Int,
): Boolean = if (size >= minSize) isNameUnsafe(name) else false
fun Tag.isNotName(
name: String,
minSize: Int,
): Boolean = !match(name, minSize)
fun Tag.matchAndHasValue(
name: String,
minSize: Int,
): Boolean = if (size >= minSize) isNameUnsafe(name) && hasValue() else false
fun Tag.valueIfMatches(
name: String,
minSize: Int,
): String? = if (match(name, minSize)) value() else null
fun Tag.valueToIntIfMatches(
name: String,
minSize: Int,
): Int? = if (match(name, minSize)) value().toIntOrNull() else null
@@ -24,6 +24,8 @@ import com.vitorpamplona.quartz.nip01Core.HexKey
typealias TagArray = Array<Array<String>>
fun <T : Event> TagArray.builder(initializer: TagArrayBuilder<T>.() -> Unit = {}) = TagArrayBuilder<T>().addAll(this).apply(initializer).build()
/**
* Performs the given [action] on each tag that matches the given [tagName].
*/
@@ -109,7 +111,7 @@ fun TagArray.mapValues(tagName: String) =
* Returns the first non-null value produced by [transform] function being applied to all tags
* that match the [tagName]
*/
fun <R> TagArray.firstMapTagged(
fun <R> TagArray.firstMappedTag(
tagName: String,
transform: (tagValue: Array<String>) -> R,
) = this.firstNotNullOfOrNull {
@@ -161,7 +163,8 @@ fun TagArray.firstTagValueFor(vararg tagNames: String) = this.firstOrNull { it.s
fun TagArray.isTagged(
tagName: String,
tagValue: String,
) = this.any { it.size > 1 && it[0] == tagName && it[1] == tagValue }
ignoreCase: Boolean = false,
) = this.any { it.size > 1 && it[0] == tagName && it[1].equals(tagValue, ignoreCase) }
/**
* Returns `true` if at least one tag matches the given [tagName] and is in [tagValues]
@@ -171,6 +174,51 @@ fun TagArray.isAnyTagged(
tagValues: Set<String>,
) = this.any { it.size > 1 && it[0] == tagName && it[1] in tagValues }
fun <U> TagArray.any(
predicate: (Array<String>, U) -> Boolean,
extras: U,
): Boolean {
for (element in this) if (predicate(element, extras)) return true
return false
}
public inline fun <T, U, R : Any> Array<out T>.firstNotNullOfOrNull(
transform: (T, U) -> R?,
extras: U,
): R? {
for (element in this) {
val result = transform(element, extras)
if (result != null) {
return result
}
}
return null
}
/**
* Returns `true` if at least one tag matches the given [tagName] and is in [tagValues]
*/
fun TagArray.firstAnyLowercaseTaggedValue(
tagName: String,
tagValues: Set<String>,
) = this.firstOrNull { it.size > 1 && it[0] == tagName && it[1].lowercase() in tagValues }?.getOrNull(1)
/**
* Returns `true` if at least one tag matches the given [tagName] and is in [tagValues]
*/
fun TagArray.isAnyLowercaseTagged(
tagName: String,
tagValues: Set<String>,
) = this.any { it.size > 1 && it[0] == tagName && it[1].lowercase() in tagValues }
/**
* Returns `true` if at least one tag matches the given [tagName] and is in [tagValues]
*/
fun TagArray.firstAnyTaggedValue(
tagName: String,
tagValues: Set<String>,
) = this.firstOrNull { it.size > 1 && it[0] == tagName && it[1] in tagValues }?.getOrNull(1)
/**
* Returns `true` if at least one tag has value that contains [text]
*/
@@ -178,3 +226,15 @@ fun TagArray.tagValueContains(
text: String,
ignoreCase: Boolean = false,
) = this.any { it.size > 1 && it[1].contains(text, ignoreCase) }
fun TagArray.containsAllTagNamesWithValues(names: Set<String>): Boolean {
val remaining = names.toMutableSet()
this.forEach {
if (it.size > 1) {
remaining.remove(it[0])
}
}
return remaining.isEmpty()
}
@@ -20,23 +20,75 @@
*/
package com.vitorpamplona.quartz.nip01Core.core
class TagArrayBuilder {
val tagList = mutableListOf<Array<String>>()
class TagArrayBuilder<T : IEvent> {
/**
* keeps a tag list by tag names to treat tags that must be unique
*/
private val tagList = mutableMapOf<String, MutableList<Tag>>()
fun add(tag: Array<String>): TagArrayBuilder {
tagList.add(tag)
fun remove(tagName: String): TagArrayBuilder<T> {
tagList.remove(tagName)
return this
}
fun addAll(tag: List<Array<String>>): TagArrayBuilder {
tagList.addAll(tag)
fun remove(
tagName: String,
tagValue: String,
): TagArrayBuilder<T> {
tagList[tagName]?.removeIf { it.value() == tagValue }
if (tagList[tagName]?.isEmpty() == true) {
tagList.remove(tagName)
}
return this
}
fun addAll(tag: Array<Array<String>>): TagArrayBuilder {
tagList.addAll(tag)
fun removeIf(
predicate: (Tag, Tag) -> Boolean,
toCompare: Tag,
): TagArrayBuilder<T> {
tagList[toCompare.name()]?.removeIf { predicate(it, toCompare) }
if (tagList[toCompare.name()]?.isEmpty() == true) {
tagList.remove(toCompare.name())
}
return this
}
fun build() = tagList.toTypedArray()
fun removeIf(
tagName: String,
tagValue: String,
): TagArrayBuilder<T> {
tagList[tagName]?.removeIf { it.value() == tagValue }
if (tagList[tagName]?.isEmpty() == true) {
tagList.remove(tagName)
}
return this
}
fun add(tag: Array<String>): TagArrayBuilder<T> {
if (tag.isEmpty() || tag[0].isEmpty()) return this
tagList.getOrPut(tag[0], ::mutableListOf).add(tag)
return this
}
fun addUnique(tag: Array<String>): TagArrayBuilder<T> {
if (tag.isEmpty() || tag[0].isEmpty()) return this
tagList[tag[0]] = mutableListOf(tag)
return this
}
fun addAll(tag: List<Array<String>>): TagArrayBuilder<T> {
tag.forEach(::add)
return this
}
fun addAll(tag: Array<Array<String>>): TagArrayBuilder<T> {
tag.forEach(::add)
return this
}
fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray()
fun build() = toTypedArray()
}
inline fun <T : Event> tagArray(initializer: TagArrayBuilder<T>.() -> Unit = {}): TagArray = TagArrayBuilder<T>().apply(initializer).build()
@@ -18,9 +18,10 @@
* 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
package com.vitorpamplona.quartz.nip01Core.crypto
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
class KeyPair(
privKey: ByteArray? = null,
@@ -18,7 +18,7 @@
* 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
package com.vitorpamplona.quartz.nip01Core.crypto
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.utils.nextBytes
@@ -35,9 +35,9 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Request
import com.vitorpamplona.quartz.nip47WalletConnect.RequestDeserializer
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip47WalletConnect.ResponseDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.Rumor
import com.vitorpamplona.quartz.nip59Giftwrap.RumorDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.RumorSerializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer
class EventMapper {
companion object {
@@ -18,17 +18,18 @@
* 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
package com.vitorpamplona.quartz.nip01Core.metadata
import android.util.Log
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
import com.vitorpamplona.quartz.nip31Alts.AltTagSerializer
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip39ExtIdentities.updateClaims
import com.vitorpamplona.quartz.utils.TimeUtils
import java.io.ByteArrayInputStream
@@ -69,7 +70,7 @@ class MetadataEvent(
val tags = mutableListOf<Array<String>>()
tags.add(
AltTagSerializer.toTagArray("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"),
AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"),
)
return signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString())
@@ -119,7 +120,7 @@ class MetadataEvent(
ObjectMapper().writeValue(writer, currentJson)
val tags = mutableListOf<Array<String>>()
tags.add(AltTagSerializer.toTagArray("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"))
tags.add(AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"))
latest?.updateClaims(twitter, github, mastodon)?.forEach {
tags.add(it)
@@ -18,7 +18,7 @@
* 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
package com.vitorpamplona.quartz.nip01Core.metadata
import androidx.compose.runtime.Stable
import com.fasterxml.jackson.annotation.JsonProperty
@@ -18,7 +18,7 @@
* 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.relays
package com.vitorpamplona.quartz.nip01Core.relay
import androidx.collection.LruCache
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -18,7 +18,7 @@
* 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.relays
package com.vitorpamplona.quartz.nip01Core.relay
enum class RelayState {
// Websocket connected
@@ -18,27 +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.nip01Core.relays
package com.vitorpamplona.quartz.nip01Core.relay
import android.util.Log
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.AuthMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.ClosedMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.EoseMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.EventMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.NoticeMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.NotifyMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.OkMessage
import com.vitorpamplona.quartz.nip01Core.relays.commands.toClient.ToClientParser
import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.AuthCmd
import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.CloseCmd
import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.CountCmd
import com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.EventCmd
import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocket
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebSocketListener
import com.vitorpamplona.quartz.nip01Core.relays.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ToClientParser
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
@@ -313,7 +313,7 @@ class SimpleClientRelay(
if (isReady) {
if (filters.isNotEmpty()) {
writeToSocket(
com.vitorpamplona.quartz.nip01Core.relays.commands.toRelay.ReqCmd
com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
.toJson(requestId, filters),
)
afterEOSEPerSubscription[requestId] = false
@@ -18,9 +18,9 @@
* 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.relays
package com.vitorpamplona.quartz.nip01Core.relay
import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.UUID
class Subscription(
@@ -18,10 +18,10 @@
* 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.relays
package com.vitorpamplona.quartz.nip01Core.relay
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relays.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
interface SubscriptionCollection {
fun isActive(subscriptionId: String): Boolean
@@ -18,7 +18,7 @@
* 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.relays.commands.toClient
package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient
import com.fasterxml.jackson.databind.JsonNode
@@ -18,7 +18,7 @@
* 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.relays.commands.toClient
package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient
import com.fasterxml.jackson.databind.JsonNode
@@ -18,7 +18,7 @@
* 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.relays.commands.toClient
package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient
import com.fasterxml.jackson.databind.JsonNode
@@ -18,7 +18,7 @@
* 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.relays.commands.toClient
package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient
import com.fasterxml.jackson.databind.JsonNode
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -18,6 +18,6 @@
* 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.relays.commands.toClient
package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient
interface Message
@@ -18,7 +18,7 @@
* 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.relays.commands.toClient
package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient
import com.fasterxml.jackson.databind.JsonNode

Some files were not shown because too many files have changed in this diff Show More