fix quartz dependencies

This commit is contained in:
greenart7c3
2023-09-08 15:59:31 -03:00
parent e879501e8f
commit eddd4668c7
96 changed files with 4 additions and 21504 deletions
+4 -1
View File
@@ -68,7 +68,10 @@ android {
} }
dependencies { dependencies {
implementation project(path: ':quartz') implementation('com.github.vitorpamplona.amethyst:quartz:main-SNAPSHOT') {
exclude group: 'net.java.dev.jna'
}
implementation 'net.java.dev.jna:jna:5.13.0@aar'
implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.core:core-splashscreen:1.0.1"
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
-1
View File
@@ -5,7 +5,6 @@ buildscript {
lifecycle_version = '2.6.1' lifecycle_version = '2.6.1'
compose_ui_version = '1.5.0' compose_ui_version = '1.5.0'
nav_version = "2.7.0" nav_version = "2.7.0"
core_ktx_version = '1.10.1'
} }
} }
-1
View File
@@ -1 +0,0 @@
/build
-58
View File
@@ -1,58 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
namespace 'com.vitorpamplona.quartz'
compileSdk 34
defaultConfig {
minSdk 26
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation "androidx.core:core-ktx:$core_ktx_version"
// @Immutable and @Stable
implementation "androidx.compose.runtime:runtime:$compose_ui_version"
// Bitcoin secp256k1 bindings to Android
api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1'
// LibSodium for XChaCha encryption
implementation "com.goterl:lazysodium-android:5.1.0@aar"
implementation 'net.java.dev.jna:jna:5.13.0@aar'
api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2'
// immutable collections to avoid recomposition
api("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// Parses URLs from Text:
api "io.github.url-detector:url-detector:0.1.23"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
View File
-60
View File
@@ -1,60 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# preserve the line number information for debugging stack traces.
-dontobfuscate
-keepattributes LocalVariableTable
-keepattributes LocalVariableTypeTable
-keepattributes *Annotation*
-keepattributes SourceFile
-keepattributes LineNumberTable
-keepattributes Signature
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes EnclosingMethod
-keepattributes MethodParameters
-keepparameternames
-keepdirectories libs
# Keep all names
-keepnames class ** { *; }
# Keep All enums
-keep enum ** { *; }
# preserve access to native classses
-keep class fr.acinq.secp256k1.** { *; }
# JNA For Libsodium
-keep class com.goterl.lazysodium.** { *; }
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
-keep class com.sun.jna.ToNativeConverter { *; }
-keep class com.sun.jna.NativeMapped { *; }
-keep class com.sun.jna.CallbackReference { *; }
-keep class com.sun.jna.ptr.IntByReference { *; }
-keep class com.sun.jna.NativeLong { *; }
-keep class com.sun.jna.Structure { *; }
-keep class com.sun.jna.Structure$* { *; }
-keep class com.sun.jna.Native$ffi_callback { *; }
-keep class * implements com.sun.jna.Structure$* { *; }
-keep class * implements com.sun.jna.Native$* { *; }
-keep class com.sun.jna.Native {
private static com.sun.jna.NativeMapped fromNative(java.lang.Class, java.lang.Object);
private static com.sun.jna.NativeMapped fromNative(java.lang.reflect.Method, java.lang.Object);
private static java.lang.Class nativeType(java.lang.Class);
private static java.lang.Object toNative(com.sun.jna.ToNativeConverter, java.lang.Object);
private static java.lang.Object fromNative(com.sun.jna.FromNativeConverter, java.lang.Object, java.lang.reflect.Method);
}
# JSON parsing
-keep class com.vitorpamplona.quartz.crypto.** { *; }
-keep class com.vitorpamplona.quartz.encoders.** { *; }
-keep class com.vitorpamplona.quartz.events.** { *; }
-keep class com.vitorpamplona.amethyst.model.** { *; }
-keep class com.vitorpamplona.amethyst.service.** { *; }
File diff suppressed because one or more lines are too long
@@ -1,20 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ChatroomKeyTest {
@Test
fun testEquals() {
val k1 = ChatroomKey(persistentSetOf("Key1", "Key2"))
val k2 = ChatroomKey(persistentSetOf("Key1", "Key2"))
assertEquals(k1, k2)
assertEquals(k1.hashCode(), k2.hashCode())
}
}
@@ -1,89 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.TextNoteEvent
import junit.framework.TestCase.assertEquals
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/"
]
}
"""
@Test
fun parseEvent() {
val event = Event.fromJson(json) as TextNoteEvent
val expectedCitations = setOf(
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
)
assertEquals(expectedCitations, event.citedUsers())
}
}
@@ -1,114 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CryptoUtilsTest {
@Test
fun testGetPublicFromPrivateKey() {
val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val publicKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
assertEquals("7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", publicKey)
}
@Test
fun testSharedSecretCompatibilityWithCoracle() {
val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561"
val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f"
val key = CryptoUtils.getSharedSecretNIP44(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray())
assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey())
}
@Test
fun testSharedSecret() {
val sender = KeyPair()
val receiver = KeyPair()
val sharedSecret1 = CryptoUtils.getSharedSecretNIP44(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = CryptoUtils.getSharedSecretNIP44(receiver.privKey!!, sender.pubKey)
assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey())
val secretKey1 = KeyPair(privKey = sharedSecret1)
val secretKey2 = KeyPair(privKey = sharedSecret2)
assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey())
assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey())
}
@Test
fun encryptDecryptNIP4Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP04(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptDecryptNIP4WithJsonSchemaTest() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP04Json(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptDecryptNIP44Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP44(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP44(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP4Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP04(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP44Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, publicKey)
val encrypted = CryptoUtils.encryptNIP44(msg, sharedSecret)
val decrypted = CryptoUtils.decryptNIP44(encrypted, sharedSecret)
assertEquals(msg, decrypted)
}
}
@@ -1,22 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.Event
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EventSigCheck {
val payload1 = "[\"EVENT\",\"40b9\",{\"id\":\"48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf\",\"kind\":1,\"pubkey\":\"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d\",\"created_at\":1677940007,\"content\":\"I got asked about follower count again today. Why does my follower count go down when I delete public relays (in our list) and replace them with filter.nostr.wine? \\n\\nI’ll give you one final explanation to rule them all. First, let’s go over how clients calculate your follower count.\\n\\n1. Your client sends a request to all your connected relays asking for accounts who follow you\\n2. Relays answer back with the events requested\\n3. The client aggregates the event total and displays it\\n\\nEach relay has a set limit on how many stored events it will return per request. For some relays it’s 500, others 1000, some as high as 5000. Let’s say for simplicity that all your public relays use 500 as their limit. If you ask 10 relays for your followers the max possible answer you can get is 5000. That won’t change if you have 20,000 followers or 100,000. You may get back a “different” 5000 each time, but you’ll still cap out at 5000 because that is the most events your client will receive.\u2028\u2028Our limit on filter.nostr.wine is 2000 events. If you replace 10 public relays with only filter.nostr.wine, the MOST followers you will ever get back from our filter relay is 2000. That doesn’t mean you only have 2000 followers or that your reach is reduced in any way.\\n\\nAs long as you are writing to and reading from the same public relays, neither your reach nor any content was lost. That concludes my TED talk. I hope you all have a fantastic day and weekend.\",\"tags\":[],\"sig\":\"dcaf8ab98bb9179017b35bd814092850d1062b26c263dff89fb1ae8c019a324139d1729012d9d05ff0a517f76b1117d869b2cc7d36bea8aa5f4b94c5e2548aa8\"}]"
@Test
fun testUnicode2028and2029ShouldNotBeEscaped() {
val msg = Event.mapper.readTree(payload1)
val event = Event.fromJson(msg[2])
// Should pass
event.checkSignature()
}
}
@@ -1,560 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.Gossip
import com.vitorpamplona.quartz.events.NIP24Factory
import com.vitorpamplona.quartz.events.SealedGossipEvent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GiftWrapEventTest {
@Test()
fun testNip24Utils() {
val sender = KeyPair()
val receiver = KeyPair()
val message = "Hola, que tal?"
val events = NIP24Factory().createMsgNIP24(
message,
listOf(receiver.pubKey.toHexKey()),
sender.privKey!!
)
// Simulate Receiver
val eventsReceiverGets = events.filter { it.isTaggedUser(receiver.pubKey.toHexKey()) }
eventsReceiverGets.forEach {
val event = it.unwrap(receiver.privKey!!)
if (event is SealedGossipEvent) {
val innerData = event.unseal(receiver.privKey!!)
assertEquals(message, innerData?.content)
} else {
fail("Wrong Event")
}
}
// Simulate Sender
val eventsSenderGets = events.filter { it.isTaggedUser(sender.pubKey.toHexKey()) }
eventsSenderGets.forEach {
val event = it.unwrap(sender.privKey!!)
if (event is SealedGossipEvent) {
val innerData = event.unseal(sender.privKey!!)
assertEquals(message, innerData?.content)
} else {
fail("Wrong Event")
}
}
}
@Test()
fun testNip24UtilsForGroups() {
val sender = KeyPair()
val receiver1 = KeyPair()
val receiver2 = KeyPair()
val receiver3 = KeyPair()
val receiver4 = KeyPair()
val message = "Hola, que tal?"
val receivers = listOf(
receiver1,
receiver2,
receiver3,
receiver4
)
val events = NIP24Factory().createMsgNIP24(
message,
receivers.map { it.pubKey.toHexKey() },
sender.privKey!!
)
// Simulate Receiver
receivers.forEach { receiver ->
val eventsReceiverGets = events.filter { it.isTaggedUser(receiver.pubKey.toHexKey()) }
eventsReceiverGets.forEach {
val event = it.unwrap(receiver.privKey!!)
if (event is SealedGossipEvent) {
val innerData = event.unseal(receiver.privKey!!)
assertEquals(message, innerData?.content)
} else {
fail("Wrong Event")
}
}
}
// Simulate Sender
val eventsSenderGets = events.filter { it.isTaggedUser(sender.pubKey.toHexKey()) }
eventsSenderGets.forEach {
val event = it.unwrap(sender.privKey!!)
if (event is SealedGossipEvent) {
val innerData = event.unseal(sender.privKey!!)
assertEquals(message, innerData?.content)
} else {
fail("Wrong Event")
}
}
}
@Test()
fun testInternalsSimpleMessage() {
val sender = KeyPair()
val receiver = KeyPair()
val senderMessage = ChatMessageEvent.create(
msg = "Hi There!",
to = listOf(receiver.pubKey.toHexKey()),
privateKey = sender.privKey!!
)
// MsgFor the Receiver
val encMsgFromSenderToReceiver = SealedGossipEvent.create(
event = senderMessage,
encryptTo = receiver.pubKey.toHexKey(),
privateKey = sender.privKey!!
)
// Should expose sender
assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey.toHexKey())
// Should not expose receiver
assertTrue(encMsgFromSenderToReceiver.tags.isEmpty())
val giftWrapEventToReceiver = GiftWrapEvent.create(
event = encMsgFromSenderToReceiver,
recipientPubKey = receiver.pubKey.toHexKey()
)
// Should not be signed by neither sender nor receiver
assertNotEquals(giftWrapEventToReceiver.pubKey, sender.pubKey.toHexKey())
assertNotEquals(giftWrapEventToReceiver.pubKey, receiver.pubKey.toHexKey())
// Should not include sender as recipient
assertNotEquals(giftWrapEventToReceiver.recipientPubKey(), sender.pubKey.toHexKey())
// Should be addressed to the receiver
assertEquals(giftWrapEventToReceiver.recipientPubKey(), receiver.pubKey.toHexKey())
// MsgFor the Sender
val encMsgFromSenderToSender = SealedGossipEvent.create(
event = senderMessage,
encryptTo = sender.pubKey.toHexKey(),
privateKey = sender.privKey!!
)
// Should expose sender
assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey.toHexKey())
// Should not expose receiver
assertTrue(encMsgFromSenderToSender.tags.isEmpty())
val giftWrapEventToSender = GiftWrapEvent.create(
event = encMsgFromSenderToSender,
recipientPubKey = sender.pubKey.toHexKey()
)
// Should not be signed by neither the sender, not the receiver
assertNotEquals(giftWrapEventToSender.pubKey, sender.pubKey.toHexKey())
assertNotEquals(giftWrapEventToSender.pubKey, receiver.pubKey.toHexKey())
// Should not be addressed to the receiver
assertNotEquals(giftWrapEventToSender.recipientPubKey(), receiver.pubKey.toHexKey())
// Should be addressed to the sender
assertEquals(giftWrapEventToSender.recipientPubKey(), sender.pubKey.toHexKey())
// Done
println(senderMessage.toJson())
println(encMsgFromSenderToReceiver.toJson())
println(giftWrapEventToReceiver.toJson())
println(giftWrapEventToSender.toJson())
// Receiver's side
// Unwrapping
val unwrappedMsgForSenderBySender = giftWrapEventToSender.unwrap(sender.privKey!!)
val unwrappedMsgForReceiverBySender = giftWrapEventToReceiver.unwrap(sender.privKey!!)
assertNotNull(unwrappedMsgForSenderBySender)
assertNull(unwrappedMsgForReceiverBySender)
val unwrappedMsgForSenderByReceiver = giftWrapEventToSender.unwrap(receiver.privKey!!)
val unwrappedMsgForReceiverByReceiver = giftWrapEventToReceiver.unwrap(receiver.privKey!!)
assertNull(unwrappedMsgForSenderByReceiver)
assertNotNull(unwrappedMsgForReceiverByReceiver)
assertEquals(SealedGossipEvent.kind, unwrappedMsgForSenderBySender?.kind)
assertEquals(SealedGossipEvent.kind, unwrappedMsgForReceiverByReceiver?.kind)
assertTrue(unwrappedMsgForSenderBySender is SealedGossipEvent)
assertTrue(unwrappedMsgForReceiverByReceiver is SealedGossipEvent)
if (unwrappedMsgForSenderBySender is SealedGossipEvent &&
unwrappedMsgForReceiverByReceiver is SealedGossipEvent
) {
val unwrappedGossipToSenderByReceiver = unwrappedMsgForSenderBySender.unseal(receiver.privKey!!)
val unwrappedGossipToReceiverByReceiver = unwrappedMsgForReceiverByReceiver.unseal(receiver.privKey!!)
assertNull(unwrappedGossipToSenderByReceiver)
assertNotNull(unwrappedGossipToReceiverByReceiver)
val unwrappedGossipToSenderBySender = unwrappedMsgForSenderBySender.unseal(sender.privKey!!)
val unwrappedGossipToReceiverBySender = unwrappedMsgForReceiverByReceiver.unseal(sender.privKey!!)
assertNotNull(unwrappedGossipToSenderBySender)
assertNull(unwrappedGossipToReceiverBySender)
assertEquals("Hi There!", unwrappedGossipToReceiverByReceiver?.content)
assertEquals("Hi There!", unwrappedGossipToSenderBySender?.content)
} else {
fail()
}
}
@Test()
fun testInternalsGroupMessage() {
val sender = KeyPair()
val receiverA = KeyPair()
val receiverB = KeyPair()
val senderMessage = ChatMessageEvent.create(
msg = "Who is going to the party tonight?",
to = listOf(receiverA.pubKey.toHexKey(), receiverB.pubKey.toHexKey()),
privateKey = sender.privKey!!
)
val encMsgFromSenderToReceiverA = SealedGossipEvent.create(
event = senderMessage,
encryptTo = receiverA.pubKey.toHexKey(),
privateKey = sender.privKey!!
)
val encMsgFromSenderToReceiverB = SealedGossipEvent.create(
event = senderMessage,
encryptTo = receiverB.pubKey.toHexKey(),
privateKey = sender.privKey!!
)
val encMsgFromSenderToSender = SealedGossipEvent.create(
event = senderMessage,
encryptTo = sender.pubKey.toHexKey(),
privateKey = sender.privKey!!
)
// Should expose sender
assertEquals(encMsgFromSenderToReceiverA.pubKey, sender.pubKey.toHexKey())
// Should not expose receiver
assertTrue(encMsgFromSenderToReceiverA.tags.isEmpty())
// Should expose sender
assertEquals(encMsgFromSenderToReceiverB.pubKey, sender.pubKey.toHexKey())
// Should not expose receiver
assertTrue(encMsgFromSenderToReceiverB.tags.isEmpty())
// Should expose sender
assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey.toHexKey())
// Should not expose receiver
assertTrue(encMsgFromSenderToSender.tags.isEmpty())
val giftWrapEventForReceiverA = GiftWrapEvent.create(
event = encMsgFromSenderToReceiverA,
recipientPubKey = receiverA.pubKey.toHexKey()
)
val giftWrapEventForReceiverB = GiftWrapEvent.create(
event = encMsgFromSenderToReceiverB,
recipientPubKey = receiverB.pubKey.toHexKey()
)
// Should not be signed by neither sender nor receiver
assertNotEquals(giftWrapEventForReceiverA.pubKey, sender.pubKey.toHexKey())
assertNotEquals(giftWrapEventForReceiverA.pubKey, receiverA.pubKey.toHexKey())
assertNotEquals(giftWrapEventForReceiverA.pubKey, receiverB.pubKey.toHexKey())
// Should not include sender as recipient
assertNotEquals(giftWrapEventForReceiverA.recipientPubKey(), sender.pubKey.toHexKey())
// Should be addressed to the receiver
assertEquals(giftWrapEventForReceiverA.recipientPubKey(), receiverA.pubKey.toHexKey())
// Should not be signed by neither sender nor receiver
assertNotEquals(giftWrapEventForReceiverB.pubKey, sender.pubKey.toHexKey())
assertNotEquals(giftWrapEventForReceiverB.pubKey, receiverA.pubKey.toHexKey())
assertNotEquals(giftWrapEventForReceiverB.pubKey, receiverB.pubKey.toHexKey())
// Should not include sender as recipient
assertNotEquals(giftWrapEventForReceiverB.recipientPubKey(), sender.pubKey.toHexKey())
// Should be addressed to the receiver
assertEquals(giftWrapEventForReceiverB.recipientPubKey(), receiverB.pubKey.toHexKey())
val giftWrapEventToSender = GiftWrapEvent.create(
event = encMsgFromSenderToSender,
recipientPubKey = sender.pubKey.toHexKey()
)
// Should not be signed by neither the sender, not the receiver
assertNotEquals(giftWrapEventToSender.pubKey, sender.pubKey.toHexKey())
assertNotEquals(giftWrapEventToSender.pubKey, receiverA.pubKey.toHexKey())
assertNotEquals(giftWrapEventToSender.pubKey, receiverB.pubKey.toHexKey())
// Should not be addressed to the receiver
assertNotEquals(giftWrapEventToSender.recipientPubKey(), receiverA.pubKey.toHexKey())
assertNotEquals(giftWrapEventToSender.recipientPubKey(), receiverB.pubKey.toHexKey())
// Should be addressed to the sender
assertEquals(giftWrapEventToSender.recipientPubKey(), sender.pubKey.toHexKey())
println(senderMessage.toJson())
println(encMsgFromSenderToReceiverA.toJson())
println(encMsgFromSenderToReceiverB.toJson())
println(giftWrapEventForReceiverA.toJson())
println(giftWrapEventForReceiverB.toJson())
println(giftWrapEventToSender.toJson())
val unwrappedMsgForSenderBySender = giftWrapEventToSender.unwrap(sender.privKey!!)
val unwrappedMsgForReceiverBySenderA = giftWrapEventForReceiverA.unwrap(sender.privKey!!)
val unwrappedMsgForReceiverBySenderB = giftWrapEventForReceiverB.unwrap(sender.privKey!!)
assertNotNull(unwrappedMsgForSenderBySender)
assertNull(unwrappedMsgForReceiverBySenderA)
assertNull(unwrappedMsgForReceiverBySenderB)
val unwrappedMsgForSenderByReceiverA = giftWrapEventToSender.unwrap(receiverA.privKey!!)
val unwrappedMsgForReceiverAByReceiverA = giftWrapEventForReceiverA.unwrap(receiverA.privKey!!)
val unwrappedMsgForReceiverBByReceiverA = giftWrapEventForReceiverB.unwrap(receiverA.privKey!!)
assertNull(unwrappedMsgForSenderByReceiverA)
assertNotNull(unwrappedMsgForReceiverAByReceiverA)
assertNull(unwrappedMsgForReceiverBByReceiverA)
val unwrappedMsgForSenderByReceiverB = giftWrapEventToSender.unwrap(receiverB.privKey!!)
val unwrappedMsgForReceiverAByReceiverB = giftWrapEventForReceiverA.unwrap(receiverB.privKey!!)
val unwrappedMsgForReceiverBByReceiverB = giftWrapEventForReceiverB.unwrap(receiverB.privKey!!)
assertNull(unwrappedMsgForSenderByReceiverB)
assertNull(unwrappedMsgForReceiverAByReceiverB)
assertNotNull(unwrappedMsgForReceiverBByReceiverB)
assertEquals(SealedGossipEvent.kind, unwrappedMsgForSenderBySender?.kind)
assertEquals(SealedGossipEvent.kind, unwrappedMsgForReceiverAByReceiverA?.kind)
assertEquals(SealedGossipEvent.kind, unwrappedMsgForReceiverBByReceiverB?.kind)
assertTrue(unwrappedMsgForSenderBySender is SealedGossipEvent)
assertTrue(unwrappedMsgForReceiverAByReceiverA is SealedGossipEvent)
assertTrue(unwrappedMsgForReceiverBByReceiverB is SealedGossipEvent)
if (unwrappedMsgForSenderBySender is SealedGossipEvent &&
unwrappedMsgForReceiverAByReceiverA is SealedGossipEvent &&
unwrappedMsgForReceiverBByReceiverB is SealedGossipEvent
) {
val unwrappedGossipToSenderByReceiverA = unwrappedMsgForSenderBySender.unseal(receiverA.privKey!!)
val unwrappedGossipToReceiverAByReceiverA = unwrappedMsgForReceiverAByReceiverA.unseal(receiverA.privKey!!)
val unwrappedGossipToReceiverBByReceiverA = unwrappedMsgForReceiverBByReceiverB.unseal(receiverA.privKey!!)
assertNull(unwrappedGossipToSenderByReceiverA)
assertNotNull(unwrappedGossipToReceiverAByReceiverA)
assertNull(unwrappedGossipToReceiverBByReceiverA)
val unwrappedGossipToSenderByReceiverB = unwrappedMsgForSenderBySender.unseal(receiverB.privKey!!)
val unwrappedGossipToReceiverAByReceiverB = unwrappedMsgForReceiverAByReceiverA.unseal(receiverB.privKey!!)
val unwrappedGossipToReceiverBByReceiverB = unwrappedMsgForReceiverBByReceiverB.unseal(receiverB.privKey!!)
assertNull(unwrappedGossipToSenderByReceiverB)
assertNull(unwrappedGossipToReceiverAByReceiverB)
assertNotNull(unwrappedGossipToReceiverBByReceiverB)
val unwrappedGossipToSenderBySender = unwrappedMsgForSenderBySender.unseal(sender.privKey!!)
val unwrappedGossipToReceiverABySender = unwrappedMsgForReceiverAByReceiverA.unseal(sender.privKey!!)
val unwrappedGossipToReceiverBBySender = unwrappedMsgForReceiverBByReceiverB.unseal(sender.privKey!!)
assertNotNull(unwrappedGossipToSenderBySender)
assertNull(unwrappedGossipToReceiverABySender)
assertNull(unwrappedGossipToReceiverBBySender)
assertEquals("Who is going to the party tonight?", unwrappedGossipToReceiverAByReceiverA?.content)
assertEquals("Who is going to the party tonight?", unwrappedGossipToReceiverBByReceiverB?.content)
assertEquals("Who is going to the party tonight?", unwrappedGossipToSenderBySender?.content)
} else {
fail()
}
}
@Test
fun testCaseFromAmethyst1() {
val json = """
{
"content":"{\"ciphertext\":\"AaTN5Mt7AOeMosjHeLfai89kmvW/qJ7W2VMttAwuh6hwRGV+ylJhpDbdVRhVmkCotbDjBgS6xioLrSDcdSngFOiVMHS5dTAP0MkQv09aZlBh/NgdmyfHHd24YlHPkDuF5Yb4Vmz7kq/vmjsNZvDrTen3TG2DcEoTV9GKexdMEqyBA4LsB2DLnWfpvOi0olDkGjPGSteTaU1nCdOtN8knoEKumrxwevvbygKphorvKX/j3ojMMb0AceJM6Cr6TRIvSsQnKGEv5V8qbC/uIrQoH3N108Fd/2SY2MWuyLKRnuak9F/w82MV13elq8ngyjcktLYM5yrPg5nrxZlyJsV8D7V/g/bvhoL+UmWe0XoCR5LXzy77SfIkgA1ePKEfGp5sD2CVIzXt9zHdFwGxAKZuyB4qwrRaAFrS2xx+Bw4nnEmF6V9NhfheSCmGzTILuTePx4ubvnYw/j8Hmqd6UvM3DBNnlJ3D6po0blirfWvMe/ea+Em4CMXfq8Iq+7r4gRx8azADygKeJ+C89GTBEvS9EvgrXCVfTMVTcFc44YAZhekOqYY1BOZgfxIV4gUiJfpMMd4B9MQv/tmnewrpTsq1reSQQcEW/mXT2cnMeCZbAIJSPg8usZ30QlrH+np+YSzFKWYDP1kThcV0ElEE2Ne8KaUUFIRE5KmhBQc/qtORefCpne5s7V7J5vLjT5rinsDzzENB1XVlmY1Icx42raP5tGAL1gOK5gRHLvtcgFQR3WcDRYaNqELiYxx41j9w9lz5e00Ttla255rZkb760KSLaBFBss6wYGiYCabVgtBNpkExpCFPPEd5eAZa5rNK2QrnojYsdxEnlicF6A+zSChLy/TbzxYwyQywDfoF9F8kBakPZkAhsciQViCii2KlieRq4OgJFZGndmnS82hyPqsoJIm22vWr1iqMvSBHo/9cLj/r+lfmGVOdgM62JHckPZjOLS0QWIb9gQiT+zXZG22+eZElMYbGXVpR1dyMaQtde8ivEVVLas6kMCVKaDTHEFglaCBXjJ3RNJv73HsG1kb0rMmOj8ltbBakjHpv7M59amavuu6SReYt\",\"nonce\":\"6anNjUdNwW6MNfoKzRZcz1R09N1h8G4L\",\"v\":1}",
"created_at":1690660515,
"id":"d90739741c2f5a8c1a03aab5dc219c0b708ed6b0566044495731cd0307cf19a5",
"kind":1059,
"pubkey":"a79b7162f8ebb9c9f7aa65a48977ab7f32aa097520bc543e4d625812154ff6af",
"sig":"9b012504e779632a2a1f55562fa9a85f8ae6245cbc149b83d25b2971249053abc77f65cc068e5d025b871d743678265fede70de4eaf5af642e675a5b6210077d",
"tags":[
[
"p",
"c55f0b0cb4dd180dd4395867b28dd4c208b709144a69fb7c31a46c369a5ad1c6"
]
]
}
""".trimIndent()
val privateKey = "de6152a85a0dea3b09a08a6f8139a314d498a7b52f7e5c28858b64270abd4c70"
val gossip = unwrapUnsealGossip(json, privateKey)
assertNotNull(gossip)
assertEquals("Hola, que tal?", gossip?.content)
}
@Test
fun testCaseFromAmethyst2() {
val json = """
{
"content":"{\"ciphertext\":\"Zb0ZNYAcDG5y7BiCWgbxY/i7rN7TxPwr3Oaste6em4VcetuenaMu2SyH6OuCCxxmIa7kFennJD8ZCrev0086azsPNutl9I6OCoOfDQb2GoFaLoJAkE/FuW0uEoEJuN72KsKj05HEjOM6nqL2KiW0pxTCNmlGpweMwpXQdm2ItWkybNpq8+b4NJUDee2czBUd9Kr2ELbPISTYzA17z1IzPXGQw8c73NL+QX9I/QZjM/agqX2x5q11SU52xiRyVd9zHf7TMctZI4QEsqDB6xi54D1bAeZlMhVdcpQRpGDfqRz3KXFlhB3Bwdc8GLgY0aLTn6tJs4qrHP3mQkxFYk0mju0afoc0rloMEUHcBVtM18S9OrTPqfmSqFTQsjaT8g+PkmeiLBo1sXsMCS62w0abSZD9OzQtciMz70ZpcWoLjx5f8panjFClvg4tJ8czMURIHM/IFS1uKAUHBArGN8QpCw8MXQBblpyLDiEkFcSX334Zdps0OIw4z328JSdeejyRh4ks+NHDt9FcjC4iicEqfEh8OTkXuKqEAVkRyfAioNQxWQPnXDzMX0Q+BXvKzBA7NaEBDpbV36H/KnrpBBQwokV9/Byb6Seh3g6GSqRAWD3U6Nk2aBMXkD0xY8vnIqMckBeYHxn8BW7k1FdXFC9lE5xCxWZHkmksJ4f0NVaF37O6d8qOe6RK7bfUeF8/SouJEu+eEX1f4KCMboslwkdk8QA8bThGcRGn8GQBMrPKrpZwHYNyyH8jwt9pywigXJejRLDDnDp3FH/3dbZy5CfuNH6KGydf/O5xx1r316so1UPO1mL5LHJUFZVIaMaMMUsgq12gpI0lLEh5NJPpsi9e3ibkzEZGf7FlAJjJQURbQ8xacN7R+w3GWKbJNHiQbUZ2lXo6fwz33t0DrSqEW970yWPHlqxcpd27EI+qqb5IqfklQZ3RObZZBhzDvImaCPG+U7SmgLhPxnilpGjd5lw/ttiqJhPG9mYFMf1eJXSG+Q9VVkGzN7jxXYtx0q0WGjVq98ZGv5RSnF1d9+QVGCd1fiPS3rsaWdYWly8l0y2quYObJ6Mv3Wh3\",\"nonce\":\"/Q2UTTjVZthm/atcCuDjU1e4reF+ZSgZ\",\"v\":1}",
"created_at":1690660515,
"id":"087d9627d63135a5050758a69222e566c86702e930c9905f0b93ccd6bebeca3f",
"kind":1059,
"pubkey":"e59c00796ae2aa9077fc8bcd57fe8d32c0fc363f7c8b93d055c70804ffff3772",
"sig":"807cb641c314ca6910aaeefadcf87d859137520be1039eb40e39832ed59d456fdd800c5f88bba09e1b395ee90c66d5330847bdd010b63be9919bf091adbc2c2a",
"tags":[
[
"p",
"f85f315c06aaf19c2b30a96ca80d9644720655ee8d3ec43b84657a7c98f36a23"
]
]
}
""".trimIndent()
val privateKey = "409ff7654141eaa16cd2161fe5bd127aeaef71f270c67587474b78998a8e3533"
val gossip = unwrapUnsealGossip(json, privateKey)
assertNotNull(gossip)
assertEquals("Hola, que tal?", gossip?.content)
}
@Test
fun testDecryptFromCoracle() {
val json = """
{
"content": "{\"ciphertext\":\"fo0/Ywyfu86cXKoOOeFFlMRv+LYM6GJUL+F/J4ARv6EcAufOZP46vimlurPBLPjNgzuGemGjlTFfC3vtq84AqIsqFo3dqKunq8Vp+mmubvxIQUDzOGYvM0WE/XOiW5LEe3U3Vq399Dq07xRpXELcp4EZxGyu4Fowv2Ppb4SKpH8g+9N3z2+bwYcSxBvI6SrL+hgmVMrRlgvsUHN1d53ni9ehRseBqrLj/DqyyEiygsKm6vnEZAPKnl1MrBaVOBZmGsyjAa/G4SBVVmk78sW7xWWvo4cV+C22FluDWUOdj/bYabH4aR4scnBX3GLYqfLuzGnuQlRNsb5unXVX41+39uXzROrmNP6iYVyYxy5tfoyN7PPZ4osoKpLDUGldmXHD6RjMcAFuou4hXt2JlTPmXpj/x8qInXId5mkmU4nTGiasvsCIpJljbCujwCjbjLTcD4QrjuhMdtSsAzjT0CDv5Lmc632eKRYtDu/9B+lkqBBkp7amhzbqp8suNTnybkvbGFQQGEQnsLfNJw/GGopAuthfi8zkTgUZR/LxFR7ZKAX73G+5PQSDSjPuGH/dQEnsFo45zsh1Xro8SfUQBsPphbX2GS31Lwu5vA30O922T4UiWuU+EdNgZR0JankQ5NPgvr1uS56C3v84VwdrNWQUCwC4eYJl4Mb/OdpEy9qwsisisppq6uuzxmxd1qx3JfocnGsvB7h2g2sG+0lyZADDSobOEZEKHaBP3w+dRcJW9D95EmzPym9GO0n+33OfqFQbda7G0rzUWfPDV0gXIuZcKs/HmDqepgIZN8FG7JhRBeAv0bCbKQACre0c8tzVEn5yCYemltScdKop3pC/r6gH50jRhAlFAiIKx8R+XwuMmJRqOcH4WfkpZlfVU85/I0XJOCHWKk6BnJi/NPP9zYiZiJe+5LecqMUVjtO0YAlv138+U/3FIT/anQ4H5bjVWBZmajwf\",\"nonce\":\"Mv70S6jgrs4D1rlqV9b5DddiymGAcVVe\",\"v\":1}",
"created_at": 1690528373,
"id": "6b108e4236c3c338236ee589388ce0f91f921e1532ae52e75d1d2add6f8e691a",
"kind": 1059,
"pubkey": "627dc0248335e2bf9adac14be9494139ebbeb12c422d7df5b0e3cd72d04c209c",
"sig": "be11da487196db298e4ffb7a03e74176c37441c88b39c95b518fadce6fd02f23c58b2c435ca38c24d512713935ab719dae80bf952267630809e1f84be8e95174",
"tags": [
[
"p",
"e7764a227c12ac1ef2db79ae180392c90903b2cec1e37f5c1a4afed38117185e"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""".trimIndent()
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
val gossip = unwrapUnsealGossip(json, privateKey)
assertNotNull(gossip)
assertEquals("test", gossip?.content)
}
@Test
fun testFromCoracle2() {
val json = """
{
"content": "{\"ciphertext\":\"Hn/dHo/I8Qk6QWWAiKyo/SfKJqQfHdV0O5tMmgqMyfHrsFoDY6IhGQP2EgCJ/6HsNQyO/8EMAmLW8w0PbDKlBKYGKGpaMwCA6B1r0rLjvu+149RJZuggRNm9rd7tNVNkNs38iqt1KYD++bohePm52q+VhAQikbX2gTONV82ROwZylAg9vjvMnYkDt45g6N97s9FRB6V7YMiUEtJnneMixa6klucpUuenQ4569tyt5vnUMD2VNhKYCc2jit2hf7k0DIhvZrVC3OdopUvxIuYYWr3r7XpuEB3HJ6Ji3ajHPzgGeFcItBR7uKZ9s6XU34F3keyZbxrv3yWHFM5NrOctAdZexSGpqWRW93M0KZUAp9HgQh3YzMLl8xt0mcrVywCgjU6Kx8IwkI0bjPU+Am8acY3cItted6hZQ4Vy1xFITdKVfPWDl3Ab59iBg9+IkY5C31wqsKPgPVVycwQE6UpaGW74gy3qZshwyoo01owvEIbVvrSJWXH7EUVvndDPvUbo+f+EVa84IEwVjPmY2oR7VsxVfqRBdmPg23OSw/9rzVybmruqaQHd3xrTTEcnG0qBc/ugCXsiuILTeScOovEnqIlKKK3KB36jMtdScdJB+b4YrzJInY1AvqU7IAgqe0vmo1LdbMtj7kjuxkXJhhQsunAbTvPigTrsOfJ08P9l7r/95kpxudgagEaW7XAjYVfLphseJT3Iy1IuQEyG5sshQ+pl/CYvkGide7ykHwm9pjSBVkD9Mdcn5X6lSnLNJEcwY43pz43r6Kq3L09qneILY3DSKyQ16Zcu1MiAMAM5r6JGvpAHqcMmixi9ORuiryjteTmY4L0vI7b3W/0RSUblXxUrb8IpeysBrFmiKJBgCoU0r/D/8tgR+Eewyp1qxKI4SfKG5GFH40zZ2oVvKyoHAR4x1oVDp/MttcnxkzAsCFL6QuJC9A/vImjsumpmYB/EChcZCOAsfqkuzH4VSjZx\",\"nonce\":\"K537d+7m5tUcXZfkr3Qk2J2G86vdBMmY\",\"v\":1}",
"created_at": 1690655012,
"id": "c4f97c6332b0a63912c44c9e1f8c7b23581dc67a8489ec1522ec205fea7133db",
"kind": 1059,
"pubkey": "8def03a22b1039256a3883d46c7ccd5562f61743100db401344284547de7ec61",
"sig": "25dcf24bdda99c04abc72274d9f7a30538a4a00a70ac4b39db4082b73823979858df93cd649c25edfb759857eac46ed70bb9ad0598f2e011d733a5a382bc4def",
"tags": [
[
"p",
"e7764a227c12ac1ef2db79ae180392c90903b2cec1e37f5c1a4afed38117185e"
]
]
}
""".trimIndent()
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
val gossip = unwrapUnsealGossip(json, privateKey)
assertEquals("asdfasdfasdf", gossip?.content)
assertEquals(1690659269L, gossip?.createdAt)
assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", gossip?.id)
assertEquals(14, gossip?.kind)
assertEquals("subject", gossip?.tags?.firstOrNull()?.get(0))
assertEquals("test", gossip?.tags?.firstOrNull()?.get(1))
}
@Test
fun testFromCoracle3() {
val json = """
{
"content": "{\"ciphertext\":\"PGCodiacmCB/sClw0C6DQRpP/XIfNAKCVZdKLQpOqytgbFryhs9Z+cPldq2FajXzxs3jQsF7/EVQyWpuV8pXetvFT9tvzjg4Xcm7ZcooLUnAeAo2xZNcJontN4cGubuDqKuXy5n59yXP1fIxfnJxRTRRdCZ2edhsKeNR5NSByUi+StjV10rnfHt8AhZCpiXiZ/giTOsC4wdaeONPgMzMeljaJWLvl6n11VjmXhkx1mXIQt43CNB1hIqO3p89Mbd9p+nlLrOsR+Xs0TB4DCh4XTPbvgf7B7Z+PgOfl3GZfJy9x6TciLcF4E3Ba1zrPe4f79czCIEiJ1yrIKrzzYvv+it35DZQ8fgveFXpyHnNL29hml8PNjyOsFbCHVYLMGw88evI5PijOcpe1TtdoioX8kX5kVEQSKJXuoSjTorvbRPCgGzaa1m0J0uTpzri5VD22a/Jh2CcAnubg6w4JDdUWCogdSV3NqiJllo7ZF7WnZ3apPdRD23MEfphVBJrcLBUNlmwajnY5IvVTKTkZOP50r9dBapvMWXIo6M6zhy/5vVWJz57863pelYCRG4upaXZuNK9sMBtbiphxmFR83i8RML8KN8Q391Cd/xBN7TxJNo5p2YU25VeGZUAmHY8DYlMQDm8Br0nStAXp3T+DzTRL8FTECa8DJV+KTAPoCxqhv3B28Ehr0XAP75CsHoLU00G48cR7h3vQ0CnfKh6KXU6nnDA5OWfpMYpirACCpsnpSD0OaCQ3gkQp3zZNMS3HcOpnPK/IY7R0esbzgAkvNhkyxaIfPDdf+eRUSOA9+2Ji28MwjjY8Dw3SLdUqCOzIDjQeR/T5oNmaQJm3lZ8G0FxxC6ejD4VJX/NI/x+STeB9jWHWmHZvqKzV6JHNh6qmZb6TKSIPOHpafWFoeJFOmiiigf46sju9vRXmVEAx59HXWnvnvCBNJg877yCMulB6xyQuSdVDuotQU4tQZwCKedTHJ6GqjesM98UlJrDtdWQURwwW1qc7N8tS6PukmUVEf0jmbIWVIBmUlkcVuiSs1g1h1kjt8c4MnGTz3CSgpOd1MqxLrl9WwrTqM+YnE+yeZYUjFoewyKZIQ==\",\"nonce\":\"OdCZczJiUGR4bOGIElQ4UUH4dQmG5U/3\",\"v\":1}",
"kind": 1059,
"created_at": 1690772945,
"pubkey": "e01475e87896800b7285eb0daf263c59f811c8fc5bc8daa105d2c98b6d7c4952",
"tags": [
[
"p",
"b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78"
]
],
"id": "d9fc85ece892ce45ffa737b3ddc0f8b752623181d75363b966191f8c03d2debe",
"sig": "1b20416b83f4b5b8eead11e29c185f46b5e76d1960e4505210ddd00f7a6973cc11268f52a8989e3799b774d5f3a55db95bed4d66a1b6e88ab54becec5c771c17"
}
""".trimIndent()
val privateKey = "7dd22cafc512c0bc363a259f6dcda515b13ae3351066d7976fd0bb79cbd0d700"
val gossip = unwrapUnsealGossip(json, privateKey)
assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", gossip?.id)
assertEquals("Howdy", gossip?.content)
assertEquals(1690833960L, gossip?.createdAt)
assertEquals(14, gossip?.kind)
assertEquals("p", gossip?.tags?.firstOrNull()?.get(0))
assertEquals("b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78", gossip?.tags?.firstOrNull()?.get(1))
assertEquals("subject", gossip?.tags?.getOrNull(1)?.get(0))
assertEquals("Stuff", gossip?.tags?.getOrNull(1)?.get(1))
}
fun unwrapUnsealGossip(json: String, privateKey: HexKey): Gossip? {
val pkBytes = privateKey.hexToByteArray()
val wrap = Event.fromJson(json) as GiftWrapEvent
wrap.checkSignature()
assertEquals(CryptoUtils.pubkeyCreate(pkBytes).toHexKey(), wrap.recipientPubKey())
val event = wrap.unwrap(pkBytes)
assertNotNull(event)
return if (event is SealedGossipEvent) {
return event.unseal(pkBytes)
} else {
println(event?.toJson())
fail("Event is not a Sealed Gossip")
null
}
}
}
@@ -1,47 +0,0 @@
package com.vitorpamplona.quartz
import com.vitorpamplona.quartz.crypto.CryptoUtils
import org.junit.Assert.assertEquals
import org.junit.Test
class HexEncodingTest {
val TestHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
@Test
fun testHexEncodeDecodeOurs() {
assertEquals(
TestHex,
com.vitorpamplona.quartz.encoders.Hex.encode(
com.vitorpamplona.quartz.encoders.Hex.decode(TestHex)
)
)
}
@Test
fun testHexEncodeDecodeSecp256k1() {
assertEquals(
TestHex,
fr.acinq.secp256k1.Hex.encode(
fr.acinq.secp256k1.Hex.decode(TestHex)
)
)
}
@Test
fun testRandoms() {
for (i in 0..1000) {
val bytes = CryptoUtils.privkeyCreate()
val hex = fr.acinq.secp256k1.Hex.encode(bytes)
assertEquals(
fr.acinq.secp256k1.Hex.encode(bytes),
com.vitorpamplona.quartz.encoders.Hex.encode(bytes)
)
assertEquals(
bytes.toList(),
com.vitorpamplona.quartz.encoders.Hex.decode(hex).toList()
)
}
}
}
@@ -1,34 +0,0 @@
package com.vitorpamplona.quartz
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.events.Event
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import java.io.InputStreamReader
@RunWith(AndroidJUnit4::class)
class LargeDBSignatureCheck {
@Test
fun insertDatabaseSample() = runBlocking {
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_short.json")
val eventArray = Event.mapper.readValue<ArrayList<Event>>(
InputStreamReader(fullDBInputStream)
) as List<Event>
var counter = 0
eventArray.forEach {
assertTrue(it.hasValidSignature())
counter ++
}
assertEquals(eventArray.size, counter)
}
}
@@ -1,24 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LnInvoiceUtilTest {
@Test
fun test100KAmountCalculation() {
val bolt11 = "lnbc1m1pjt9u0qsp553q90pj5mafzv20w45eqavned9tgwhl4q99n9s5ppcw24nzw3zeqpp5002kd3ktym67du86kj665fgaev7ka8ys7j5yz5fg686lr5e2gfkshp5dkk27nnuax05az3pk2r6ytxtvwn5j4xzsq9ajprhc7crjkmgvr3qxqyjw5qcqpjrzjqtzxvfsuxe4l92pf97tt4rcgpy2xalkmlwexh899wqxf83l8nwv4xzh0gvqq89qqqqqqqqlgqqqqq0gqvs9qxpqysgqx5mz04wd7kqu5zhhel9enr036hjrp4gga0nz084p2asjl36a0zmrk6mhqa249zsgqref2rlvhffm73u7rxgr47gden6rugup4ksvpzsqvds4pz"
// Context of the app under test.
Assert.assertEquals(100000, LnInvoiceUtil.getAmountInSats(bolt11).toLong())
}
@Test
fun test100GAmountCalculation() {
val bolt11 = "lnbc1000000000000000p1pjtxqf0pp5myqxhcufqy56elfsg9dd4dthnqptusnnpwnul7u86l95xzjgqd8shp5gueg34sgm3u3nxqjqyunvvqdu0pr6jz6mwh4ew4886f2lpf4cmrqxqztgsp5w0cdfd45dfnqwex5gn85x7fru3jcrxhlcx3enx835477m3gdfcuq9qyyssqelrcmm7p9qazgjuxtdg7sd8nq5cscl2tratjlclt5rk5mc7uq2lphq3r2a43j5ua4leakc4emq8yp2yxdnzvzszpw6u2afac0kgl7hspfj67ta"
// Context of the app under test.
Assert.assertEquals(100000000000, LnInvoiceUtil.getAmountInSats(bolt11).toLong())
}
}
@@ -1,134 +0,0 @@
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent.Companion.createEncryptionPrivateKey
import com.vitorpamplona.quartz.encoders.Hex
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PrivateZapTests {
@Test
fun testPollZap() {
val poll = Event.fromJson(
"""{
"content": "New poll \n\n #zappoll",
"created_at": 1682440713,
"id": "16291ba452bb0786a4bf5c278d38de73c96b58c056ed75c5ea466b0795197288",
"kind": 6969,
"pubkey": "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
"sig": "ac05fa4004c3f7c42913c87b11bf9714bb61a3f0940863a6b9ff0f8105b399add72dbc09bf944c79b9a72ef009ec6905adedbd2c4c8fb3d2f57007bad8fcb279",
"tags": [
[
"poll_option",
"0",
"Test 1"
],
[
"poll_option",
"1",
"Test 2"
],
[
"value_maximum",
"null"
],
[
"value_minimum",
"null"
],
[
"consensus_threshold",
"null"
],
[
"closed_at",
"null"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""")
val loggedIn = Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")
val privateZapRequest = LnZapRequestEvent.create(
poll,
setOf("wss://relay.damus.io/"),
loggedIn,
0,
"",
LnZapEvent.ZapType.PRIVATE
)
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
if (recepientPK != null && recepientPost != null) {
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
val decodedPrivateZap =
privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
println(decodedPrivateZap?.toJson())
assertNotNull(decodedPrivateZap)
} else {
fail("Should not be null")
}
}
@Test
fun testKind1PrivateZap() {
val textNote = Event.fromJson(
"""{
"content": "Testing copied author. \n\nnostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
"created_at": 1682369982,
"id": "c757e1371d715c711ec9ef9740a3df6475d64b3d0af45ffcbfca08d273baf1c1",
"kind": 1,
"pubkey": "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
"sig": "1fb5b6fd980f4c2ef058d5f4f7b166c0e5fb21eff26fe9cacd87a9aa4feb344485841ebcc26a233bf8d6ea0a66acf0db2bfdb11ad1cb04bcea4cfa3e78c3eaf1",
"tags": [
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""")
val loggedIn = Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")
val privateZapRequest = LnZapRequestEvent.create(
textNote,
setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"),
loggedIn,
null,
"test",
LnZapEvent.ZapType.PRIVATE
)
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
if (recepientPK != null && recepientPost != null) {
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
println(decodedPrivateZap?.toJson())
assertNotNull(decodedPrivateZap)
} else {
fail("Should not be null")
}
}
}
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -1,304 +0,0 @@
package com.vitorpamplona.quartz.crypto
import android.util.Log
import android.util.LruCache
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
private val sharedKeyCache04 = LruCache<Int, ByteArray>(200)
private val sharedKeyCache44 = LruCache<Int, ByteArray>(200)
private val secp256k1 = Secp256k1.get()
private val libSodium = SodiumAndroid()
private val random = SecureRandom()
private val h02 = Hex.decode("02")
fun clearCache() {
sharedKeyCache04.evictAll()
sharedKeyCache44.evictAll()
}
fun randomInt(bound: Int): Int {
return random.nextInt(bound)
}
/**
* Provides a 32B "private key" aka random number
*/
fun privkeyCreate() = random(32)
fun random(size: Int): ByteArray {
val bytes = ByteArray(size)
random.nextBytes(bytes)
return bytes
}
fun pubkeyCreate(privKey: ByteArray) =
secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun sign(data: ByteArray, privKey: ByteArray): ByteArray =
secp256k1.signSchnorr(data, privKey, null)
fun verifySignature(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray
): Boolean {
return secp256k1.verifySchnorr(signature, hash, pubKey)
}
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
fun encryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val info = encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
val encryptionInfo = EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
return "${encryptionInfo.ciphertext}?iv=${encryptionInfo.nonce}"
}
fun encryptNIP04Json(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
return encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
}
fun encryptNIP04(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val iv = ByteArray(16)
random.nextBytes(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
//val ivBase64 = Base64.getEncoder().encodeToString(iv)
val encryptedMsg = cipher.doFinal(msg.toByteArray())
//val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg)
return EncryptedInfo(encryptedMsg, iv, Nip44Version.NIP04.versionCode)
}
fun decryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecretNIP04(privateKey, pubKey)
return decryptNIP04(msg, sharedSecret)
}
fun decryptNIP04(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecretNIP04(privateKey, pubKey)
return decryptNIP04(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret)
}
fun decryptNIP04(msg: String, sharedSecret: ByteArray): String {
val parts = msg.split("?iv=")
return decryptNIP04(parts[0], parts[1], sharedSecret)
}
private fun decryptNIP04(cipher: String, nonce: String, sharedSecret: ByteArray): String {
val iv = Base64.getDecoder().decode(nonce)
val encryptedMsg = Base64.getDecoder().decode(cipher)
return decryptNIP04(encryptedMsg, iv, sharedSecret)
}
private fun decryptNIP04(encryptedMsg: ByteArray, iv: ByteArray, sharedSecret: ByteArray): String {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
return String(cipher.doFinal(encryptedMsg))
}
fun encryptNIP44(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
val sharedSecret = getSharedSecretNIP44(privateKey, pubKey)
return encryptNIP44(msg, sharedSecret)
}
fun encryptNIP44(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val nonce = ByteArray(24)
random.nextBytes(nonce)
val cipher = cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = msg.toByteArray(),//compress(msg),
nonce = nonce,
key = Key.fromBytes(sharedSecret)
)
return EncryptedInfo(
ciphertext = cipher ?: ByteArray(0),
nonce = nonce,
v = Nip44Version.NIP44.versionCode
)
}
fun decryptNIP44(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
val sharedSecret = getSharedSecretNIP44(privateKey, pubKey)
return decryptNIP44(encryptedInfo, sharedSecret)
}
fun decryptNIP44(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? {
return cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret)
)?.decodeToString() //?.let { decompress(it) }
}
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache04[hash]
if (preComputed != null) return preComputed
val computed = computeSharedSecretNIP04(privateKey, pubKey)
sharedKeyCache04.put(hash, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache44[hash]
if (preComputed != null) return preComputed
val computed = computeSharedSecretNIP44(privateKey, pubKey)
sharedKeyCache44.put(hash, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33))
}
data class EncryptedInfo(val ciphertext: ByteArray, val nonce: ByteArray, val v: Int)
data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v: Int)
enum class Nip44Version(val versionCode: Int) {
NIP04(0),
NIP44(1)
}
fun encodeNIP44(info: EncryptedInfo): String {
return encodeByteArray(info)
}
fun decodeNIP44(str: String): EncryptedInfo? {
if (str.isEmpty()) return null
return if (str[0] == '{') {
decodeJackson(str)
} else {
decodeByteArray(str)
}
}
fun encodeByteArray(info: EncryptedInfo): String {
return Base64.getEncoder().encodeToString(byteArrayOf(info.v.toByte()) + info.nonce + info.ciphertext)
}
fun decodeByteArray(base64: String): EncryptedInfo? {
return try {
val byteArray = Base64.getDecoder().decode(base64)
return EncryptedInfo(
v = byteArray[0].toInt(),
nonce = byteArray.copyOfRange(1, 25),
ciphertext = byteArray.copyOfRange(25, byteArray.size)
)
} catch (e: Exception) {
Log.w("CryptoUtils", "Unable to Parse encrypted payload: ${base64}")
null
}
}
fun encodeJackson(info: EncryptedInfo): String {
return Event.mapper.writeValueAsString(
EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
)
}
fun decodeJackson(json: String): EncryptedInfo {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
return EncryptedInfo(
v = info.v,
nonce = Base64.getDecoder().decode(info.nonce),
ciphertext = Base64.getDecoder().decode(info.ciphertext)
)
}
fun combinedHashCode(a: ByteArray, b: ByteArray): Int {
var result = 1
for (element in a) result = 31 * result + element
for (element in b) result = 31 * result + element
return result
}
/*
OLD Versions used for the Benchmark
fun encodeKotlin(info: EncryptedInfo): String {
return Json.encodeToString(
EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
)
}
fun decodeKotlin(json: String): EncryptedInfo {
val info = Json.decodeFromString<EncryptedInfoString>(json)
return EncryptedInfo(
v = info.v,
nonce = Base64.getDecoder().decode(info.nonce),
ciphertext = Base64.getDecoder().decode(info.ciphertext)
)
}
fun encodeCSV(info: EncryptedInfo): String {
return "${info.v},${Base64.getEncoder().encodeToString(info.nonce)},${Base64.getEncoder().encodeToString(info.ciphertext)}"
}
fun decodeCSV(base64: String): EncryptedInfo {
val parts = base64.split(",")
return EncryptedInfo(
v = parts[0].toInt(),
nonce = Base64.getDecoder().decode(parts[1]),
ciphertext = Base64.getDecoder().decode(parts[2])
)
}
fun compress(input: String): ByteArray {
return DeflaterInputStream(input.toByteArray().inputStream()).readBytes()
}
fun decompress(inputBytes: ByteArray): String {
return InflaterInputStream(inputBytes.inputStream()).bufferedReader().use { it.readText() }
}
*/
@@ -1,34 +0,0 @@
package com.vitorpamplona.quartz.crypto
import com.vitorpamplona.quartz.encoders.toHexKey
class KeyPair(
privKey: ByteArray? = null,
pubKey: ByteArray? = null
) {
val privKey: ByteArray?
val pubKey: ByteArray
init {
if (privKey == null) {
if (pubKey == null) {
// create new, random keys
this.privKey = CryptoUtils.privkeyCreate()
this.pubKey = CryptoUtils.pubkeyCreate(this.privKey)
} else {
// this is a read-only account
check(pubKey.size == 32)
this.privKey = null
this.pubKey = pubKey
}
} else {
// as private key is provided, ignore the public key and set keys according to private key
this.privKey = privKey
this.pubKey = CryptoUtils.pubkeyCreate(privKey)
}
}
override fun toString(): String {
return "KeyPair(privateKey=${privKey?.toHexKey()}, publicKey=${pubKey.toHexKey()}"
}
}
@@ -1,85 +0,0 @@
package com.vitorpamplona.quartz.crypto
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
/**
* I initially extended these methods from the Sodium and SodiumAndroid classes
* But JNI doesn't like it. There is some native method overriding bug
* when using Kotlin extensions
**/
fun /*Sodium.*/crypto_stream_xchacha20_xor_ic(
libSodium: SodiumAndroid,
cipher: ByteArray,
message: ByteArray,
messageLen: Long,
nonce: ByteArray,
ic: Long,
key: ByteArray
): Int {
/**
* C++ Code:
*
* unsigned char k2[crypto_core_hchacha20_OUTPUTBYTES];
* crypto_core_hchacha20(k2, n, k, NULL);
* return crypto_stream_chacha20_xor_ic(
* c, m, mlen, n + crypto_core_hchacha20_INPUTBYTES, ic, k2);
*/
val k2 = ByteArray(32)
val nonceChaCha = nonce.drop(16).toByteArray()
assert(nonceChaCha.size == 8)
libSodium.crypto_core_hchacha20(k2, nonce, key, null)
return libSodium.crypto_stream_chacha20_xor_ic(
cipher,
message,
messageLen,
nonceChaCha,
ic,
k2
)
}
fun /*Sodium.*/crypto_stream_xchacha20_xor(
libSodium: SodiumAndroid,
cipher: ByteArray,
message: ByteArray,
messageLen: Long,
nonce: ByteArray,
key: ByteArray
): Int {
return crypto_stream_xchacha20_xor_ic(libSodium, cipher, message, messageLen, nonce, 0, key)
}
fun /*SodiumAndroid.*/cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid,
cipher: ByteArray,
message: ByteArray,
messageLen: Long,
nonce: ByteArray,
key: ByteArray
): Boolean {
require(!(messageLen < 0 || messageLen > message.size)) { "messageLen out of bounds: $messageLen" }
return crypto_stream_xchacha20_xor(
libSodium,
cipher,
message,
messageLen,
nonce,
key
) == 0
}
fun /*SodiumAndroid.*/cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid,
messageBytes: ByteArray,
nonce: ByteArray,
key: Key
): ByteArray? {
val mLen = messageBytes.size
val cipher = ByteArray(mLen)
val sucessful = cryptoStreamXChaCha20Xor(libSodium, cipher, messageBytes, mLen.toLong(), nonce, key.asBytes)
return if (sucessful) cipher else null
}
@@ -1,68 +0,0 @@
package com.vitorpamplona.quartz.encoders
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.Hex
@Immutable
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String {
return TlvBuilder().apply {
addString(Nip19.TlvTypes.SPECIAL, dTag)
addStringIfNotNull(Nip19.TlvTypes.RELAY, relay)
addHex(Nip19.TlvTypes.AUTHOR, pubKeyHex)
addInt(Nip19.TlvTypes.KIND, kind)
}.build().toNAddress()
}
companion object {
fun isATag(key: String): Boolean {
return key.startsWith("naddr1") || key.contains(":")
}
fun parse(address: String, relay: String?): ATag? {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) {
parseNAddr(address)
} else {
parseAtag(address, relay)
}
}
fun parseAtag(atag: String, relay: String?): ATag? {
return try {
val parts = atag.split(":")
Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2], relay)
} catch (t: Throwable) {
Log.w("ATag", "Error parsing A Tag: $atag: ${t.message}")
null
}
}
fun parseNAddr(naddr: String): ATag? {
try {
val key = naddr.removePrefix("nostr:")
if (key.startsWith("naddr")) {
val tlv = Tlv.parse(key.bechToBytes())
val d = tlv.firstAsString(Nip19.TlvTypes.SPECIAL) ?: ""
val relay = tlv.firstAsString(Nip19.TlvTypes.RELAY)
val author = tlv.firstAsHex(Nip19.TlvTypes.AUTHOR)
val kind = tlv.firstAsInt(Nip19.TlvTypes.KIND)
if (kind != null && author != null) {
return ATag(kind, author, d, relay)
}
}
} catch (e: Throwable) {
Log.w("ATag", "Issue trying to Decode NIP19 $this: ${e.message}")
// e.printStackTrace()
}
return null
}
}
}
@@ -1,242 +0,0 @@
package com.vitorpamplona.quartz.encoders
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import kotlin.jvm.JvmStatic
/**
* Bech32 works with 5 bits values, we use this type to make it explicit: whenever you see Int5 it means 5 bits values,
* and whenever you see Byte it means 8 bits values.
*/
private typealias Int5 = Byte
/**
* Bech32 and Bech32m address formats.
* See https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki and https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki.
*/
object Bech32 {
const val alphabet: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
const val alphabetUpperCase: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L"
enum class Encoding(public val constant: Int) {
Bech32(1),
Bech32m(0x2bc830a3),
Beck32WithoutChecksum(0)
}
// char -> 5 bits value
private val map = Array<Int5>(255) { -1 }
init {
for (i in 0..alphabet.lastIndex) {
map[alphabet[i].code] = i.toByte()
}
for (i in 0..alphabetUpperCase.lastIndex) {
map[alphabetUpperCase[i].code] = i.toByte()
}
}
fun expand(hrp: String): Array<Int5> {
val half = hrp.length + 1
val size = half + hrp.length
return Array<Int5>(size) {
when (it) {
in hrp.indices -> hrp[it].code.shr(5).toByte()
in half until size -> (hrp[it - half].code and 31).toByte()
else -> 0
}
}
}
private val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
fun polymod(values: Array<Int5>, values1: Array<Int5>): Int {
var chk = 1
values.forEach { v ->
val b = chk shr 25
chk = ((chk and 0x1ffffff) shl 5) xor v.toInt()
for (i in 0..5) {
if (((b shr i) and 1) != 0) chk = chk xor GEN[i]
}
}
values1.forEach { v ->
val b = chk shr 25
chk = ((chk and 0x1ffffff) shl 5) xor v.toInt()
for (i in 0..5) {
if (((b shr i) and 1) != 0) chk = chk xor GEN[i]
}
}
return chk
}
/**
* @param hrp human readable prefix
* @param int5s 5-bit data
* @param encoding encoding to use (bech32 or bech32m)
* @return hrp + data encoded as a Bech32 string
*/
@JvmStatic
public fun encode(hrp: String, int5s: ArrayList<Int5>, encoding: Encoding): String {
require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) { "mixed case strings are not valid bech32 prefixes" }
val dataWithChecksum = when (encoding) {
Encoding.Beck32WithoutChecksum -> int5s
else -> addChecksum(hrp, int5s, encoding)
}
val charArray = CharArray(dataWithChecksum.size) {
alphabet[dataWithChecksum[it].toInt()]
}.concatToString()
return hrp + "1" + charArray
}
/**
* @param hrp human readable prefix
* @param data data to encode
* @param encoding encoding to use (bech32 or bech32m)
* @return hrp + data encoded as a Bech32 string
*/
@JvmStatic
public fun encodeBytes(hrp: String, data: ByteArray, encoding: Encoding): String = encode(hrp, eight2five(data), encoding)
/**
* decodes a bech32 string
* @param bech32 bech32 string
* @param noChecksum if true, the bech32 string doesn't have a checksum
* @return a (hrp, data, encoding) tuple
*/
@JvmStatic
public fun decode(bech32: String, noChecksum: Boolean = false): Triple<String, Array<Int5>, Encoding> {
var pos = 0
bech32.forEachIndexed { index, char ->
require( char.code in 33..126 ) { "invalid character ${char}" }
if (char == '1') {
pos = index
}
}
val hrp = bech32.take(pos)
require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
val data = Array(bech32.length - pos - 1) {
map[bech32[pos + 1 + it].code]
}
return if (noChecksum) {
Triple(hrp, data, Encoding.Beck32WithoutChecksum)
} else {
val encoding = when (polymod(expand(hrp), data)) {
Encoding.Bech32.constant -> Encoding.Bech32
Encoding.Bech32m.constant -> Encoding.Bech32m
else -> throw IllegalArgumentException("invalid checksum for $bech32")
}
Triple(hrp, data.copyOfRange(0, data.size-6), encoding)
}
}
/**
* decodes a bech32 string
* @param bech32 bech32 string
* @param noChecksum if true, the bech32 string doesn't have a checksum
* @return a (hrp, data, encoding) tuple
*/
@JvmStatic
public fun decodeBytes(bech32: String, noChecksum: Boolean = false): Triple<String, ByteArray, Encoding> {
val (hrp, int5s, encoding) = decode(bech32, noChecksum)
return Triple(hrp, five2eight(int5s, 0), encoding)
}
val ZEROS = arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())
/**
* @param hrp Human Readable Part
* @param data data (a sequence of 5 bits integers)
* @param encoding encoding to use (bech32 or bech32m)
* @return a checksum computed over hrp and data
*/
private fun addChecksum(hrp: String, data: ArrayList<Int5>, encoding: Encoding): ArrayList<Int5> {
val values = expand(hrp) + data
val poly = polymod(values, ZEROS) xor encoding.constant
for (i in 0 until 6) {
data.add((poly.shr(5 * (5 - i)) and 31).toByte())
}
return data
}
/**
* @param input a sequence of 8 bits integers
* @return a sequence of 5 bits integers
*/
@JvmStatic
public fun eight2five(input: ByteArray): ArrayList<Int5> {
var buffer = 0L
val output = ArrayList<Int5>(input.size * 2) //larger array on purpose. Checksum is added later.
var count = 0
input.forEach { b ->
buffer = (buffer shl 8) or (b.toLong() and 0xff)
count += 8
while (count >= 5) {
output.add(((buffer shr (count - 5)) and 31).toByte())
count -= 5
}
}
if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte())
return output
}
/**
* @param input a sequence of 5 bits integers
* @return a sequence of 8 bits integers
*/
@JvmStatic
public fun five2eight(input: Array<Int5>, offset: Int): ByteArray {
var buffer = 0L
val output = ArrayList<Byte>(input.size)
var count = 0
for (i in offset..input.lastIndex) {
val b = input[i]
buffer = (buffer shl 5) or (b.toLong() and 31)
count += 5
while (count >= 8) {
output.add(((buffer shr (count - 8)) and 0xff).toByte())
count -= 8
}
}
require(count <= 4) { "Zero-padding of more than 4 bits" }
require((buffer and ((1L shl count) - 1L)) == 0L) { "Non-zero padding in 8-to-5 conversion" }
return output.toByteArray()
}
}
fun ByteArray.toNsec() = Bech32.encodeBytes(hrp = "nsec", this, Bech32.Encoding.Bech32)
fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32)
fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32)
fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32)
fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32)
fun String.bechToBytes(hrp: String? = null): ByteArray {
val decodedForm = Bech32.decodeBytes(this)
hrp?.also {
if (it != decodedForm.first) {
throw IllegalArgumentException("Expected $it but obtained ${decodedForm.first}")
}
}
return decodedForm.second
}
@@ -1,70 +0,0 @@
package com.vitorpamplona.quartz.encoders
/** Makes the distinction between String and Hex **/
typealias HexKey = String
fun ByteArray.toHexKey(): HexKey {
return Hex.encode(this)
}
fun HexKey.hexToByteArray(): ByteArray {
return Hex.decode(this)
}
object HexValidator {
private fun isHex2(c: Char): Boolean {
return when (c) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', ' ' -> true
else -> false
}
}
fun isHex(hex: String?): Boolean {
if (hex == null) return false
var isHex = true
for (c in hex.toCharArray()) {
if (!isHex2(c)) {
isHex = false
break
}
}
return isHex
}
}
object Hex {
private val hexCode = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
// Faster if no calculations are needed.
private fun hexToBin(ch: Char): Int = when (ch) {
in '0'..'9' -> ch - '0'
in 'a'..'f' -> ch - 'a' + 10
in 'A'..'F' -> ch - 'A' + 10
else -> throw IllegalArgumentException("illegal hex character: $ch")
}
@JvmStatic
fun decode(hex: String): ByteArray {
// faster version of hex decoder
require(hex.length % 2 == 0)
val outSize = hex.length / 2
val out = ByteArray(outSize)
for (i in 0 until outSize) {
out[i] = (hexToBin(hex[2 * i]) * 16 + hexToBin(hex[2 * i + 1])).toByte()
}
return out
}
@JvmStatic
fun encode(input: ByteArray): String {
val len = input.size
val out = CharArray(len * 2)
for (i in 0 until len) {
out[i*2] = hexCode[(input[i].toInt() shr 4) and 0xF]
out[i*2+1] = hexCode[input[i].toInt() and 0xF]
}
return String(out)
}
}
@@ -1,173 +0,0 @@
package com.vitorpamplona.quartz.encoders
import java.math.BigDecimal
import java.util.Locale
import java.util.regex.Pattern
/** based on litecoinj */
object LnInvoiceUtil {
private val invoicePattern = Pattern.compile("lnbc((?<amount>\\d+)(?<multiplier>[munp])?)?1[^1\\s]+", Pattern.CASE_INSENSITIVE)
/** The Bech32 character set for encoding. */
private const val CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
/** The Bech32 character set for decoding. */
private val CHARSET_REV = byteArrayOf(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
)
/** Find the polynomial with value coefficients mod the generator as 30-bit. */
private fun polymod(values: ByteArray): Int {
var c = 1
for (v_i in values) {
val c0 = c ushr 25 and 0xff
c = c and 0x1ffffff shl 5 xor (v_i.toInt() and 0xff)
if (c0 and 1 != 0) c = c xor 0x3b6a57b2
if (c0 and 2 != 0) c = c xor 0x26508e6d
if (c0 and 4 != 0) c = c xor 0x1ea119fa
if (c0 and 8 != 0) c = c xor 0x3d4233dd
if (c0 and 16 != 0) c = c xor 0x2a1462b3
}
return c
}
/** Expand a HRP for use in checksum computation. */
private fun expandHrp(hrp: String): ByteArray {
val hrpLength = hrp.length
val ret = ByteArray(hrpLength * 2 + 1)
for (i in 0 until hrpLength) {
val c = hrp[i].code and 0x7f // Limit to standard 7-bit ASCII
ret[i] = (c ushr 5 and 0x07).toByte()
ret[i + hrpLength + 1] = (c and 0x1f).toByte()
}
ret[hrpLength] = 0
return ret
}
/** Verify a checksum. */
private fun verifyChecksum(hrp: String, values: ByteArray): Boolean {
val hrpExpanded: ByteArray = expandHrp(hrp)
val combined = ByteArray(hrpExpanded.size + values.size)
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.size)
System.arraycopy(values, 0, combined, hrpExpanded.size, values.size)
return polymod(combined) == 1
}
class AddressFormatException(message: String) : Exception(message)
fun decodeUnlimitedLength(invoice: String): Boolean {
var lower = false
var upper = false
for (i in 0 until invoice.length) {
val c = invoice[i]
if (c.code < 33 || c.code > 126) throw AddressFormatException("Invalid character: $c, pos: $i")
if (c in 'a'..'z') {
if (upper) throw AddressFormatException("Invalid character: $c, pos: $i")
lower = true
}
if (c in 'A'..'Z') {
if (lower) throw AddressFormatException("Invalid character: $c, pos: $i")
upper = true
}
}
val pos = invoice.lastIndexOf('1')
if (pos < 1) throw AddressFormatException("Missing human-readable part")
val dataPartLength = invoice.length - 1 - pos
if (dataPartLength < 6) throw AddressFormatException("Data part too short: $dataPartLength")
val values = ByteArray(dataPartLength)
for (i in 0 until dataPartLength) {
val c = invoice[i + pos + 1]
if (CHARSET_REV.get(c.code).toInt() == -1) throw AddressFormatException("Invalid character: " + c + ", pos: " + (i + pos + 1))
values[i] = CHARSET_REV.get(c.code)
}
val hrp = invoice.substring(0, pos).lowercase(Locale.ROOT)
if (!verifyChecksum(hrp, values)) throw AddressFormatException("Invalid Checksum")
return true
}
/**
* Parses invoice amount according to
* https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#human-readable-part
* @return invoice amount in bitcoins, zero if the invoice has no amount
* @throws RuntimeException if invoice format is incorrect
*/
private fun getAmount(invoice: String): BigDecimal {
try {
decodeUnlimitedLength(invoice) // checksum must match
} catch (e: AddressFormatException) {
throw IllegalArgumentException("Cannot decode invoice: $invoice", e)
}
val matcher = invoicePattern.matcher(invoice)
require(matcher.matches()) { "Failed to match HRP pattern" }
val amountGroup = matcher.group("amount")
val multiplierGroup = matcher.group("multiplier")
if (amountGroup == null) {
return BigDecimal.ZERO
}
val amount = BigDecimal(amountGroup)
if (multiplierGroup == null) {
return amount
}
require(!(multiplierGroup == "p" && amountGroup[amountGroup.length - 1] != '0')) { "sub-millisatoshi amount" }
return amount.multiply(multiplier(multiplierGroup))
}
fun getAmountInSats(invoice: String): BigDecimal {
return getAmount(invoice).multiply(BigDecimal(100000000))
}
private fun multiplier(multiplier: String): BigDecimal {
return when (multiplier.lowercase()) {
"m" -> BigDecimal("0.001")
"u" -> BigDecimal("0.000001")
"n" -> BigDecimal("0.000000001")
"p" -> BigDecimal("0.000000000001")
else -> throw IllegalArgumentException("Invalid multiplier: $multiplier")
}
}
/**
* Finds LN invoice in the provided input string and returns it.
* For example for input = "aaa bbb lnbc1xxx ccc" it will return "lnbc1xxx"
* It will only return the first invoice found in the input.
*
* @return the invoice if it was found. null for null input or if no invoice is found
*/
fun findInvoice(input: String?): String? {
if (input == null) {
return null
}
val matcher = invoicePattern.matcher(input)
return if (matcher.find()) {
matcher.group()
} else {
null
}
}
/**
* If the string contains an LN invoice, returns a Pair of the start and end
* positions of the invoice in the string. Otherwise, returns (0, 0). This is
* used to ensure we don't accidentally cut an invoice in the middle when taking
* only a portion of the available text.
*/
fun locateInvoice(input: String?): Pair<Int, Int> {
if (input == null) {
return Pair(0, 0)
}
val matcher = invoicePattern.matcher(input)
return if (matcher.find()) {
Pair(matcher.start(), matcher.end())
} else {
Pair(0, 0)
}
}
}
@@ -1,29 +0,0 @@
package com.vitorpamplona.quartz.encoders
import java.util.regex.Pattern
object LnWithdrawalUtil {
private val withdrawalPattern = Pattern.compile(
"lnurl.+",
Pattern.CASE_INSENSITIVE
)
/**
* Finds LN withdrawal in the provided input string and returns it.
* For example for input = "aaa bbb lnbc1xxx ccc" it will return "lnbc1xxx"
* It will only return the first withdrawal found in the input.
*
* @return the invoice if it was found. null for null input or if no invoice is found
*/
fun findWithdrawal(input: String?): String? {
if (input == null) {
return null
}
val matcher = withdrawalPattern.matcher(input)
return if (matcher.find()) {
matcher.group()
} else {
null
}
}
}
@@ -1,178 +0,0 @@
package com.vitorpamplona.quartz.encoders
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import java.util.regex.Pattern
object Nip19 {
enum class Type {
USER, NOTE, EVENT, RELAY, ADDRESS
}
enum class TlvTypes(val id: Byte) {
SPECIAL(0),
RELAY(1),
AUTHOR(2),
KIND(3);
}
val nip19regex = Pattern.compile(
"(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)",
Pattern.CASE_INSENSITIVE
)
@Immutable
data class Return(
val type: Type,
val hex: String,
val relay: String? = null,
val author: String? = null,
val kind: Int? = null,
val additionalChars: String = ""
)
fun uriToRoute(uri: String?): Return? {
if (uri == null) return null
try {
val matcher = nip19regex.matcher(uri)
if (!matcher.find()) {
return null
}
val uriScheme = matcher.group(1) // nostr:
val type = matcher.group(2) // npub1
val key = matcher.group(3) // bech32
val additionalChars = matcher.group(4) // additional chars
return parseComponents(uriScheme, type, key, additionalChars)
} catch (e: Throwable) {
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
}
return null
}
fun parseComponents(
uriScheme: String?,
type: String,
key: String?,
additionalChars: String?
): Return? {
return try {
val bytes = (type + key).bechToBytes()
val parsed = when (type.lowercase()) {
"npub1" -> npub(bytes)
"note1" -> note(bytes)
"nprofile1" -> nprofile(bytes)
"nevent1" -> nevent(bytes)
"nrelay1" -> nrelay(bytes)
"naddr1" -> naddr(bytes)
else -> null
}
parsed?.copy(additionalChars = additionalChars ?: "")
} catch (e: Throwable) {
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)
null
}
}
private fun npub(bytes: ByteArray): Return {
return Return(Type.USER, bytes.toHexKey())
}
private fun note(bytes: ByteArray): Return {
return Return(Type.NOTE, bytes.toHexKey())
}
private fun nprofile(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
val hex = tlv.firstAsHex(TlvTypes.SPECIAL) ?: return null
val relay = tlv.firstAsString(TlvTypes.RELAY)
return Return(Type.USER, hex, relay)
}
private fun nevent(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
val hex = tlv.firstAsHex(TlvTypes.SPECIAL) ?: return null
val relay = tlv.firstAsString(TlvTypes.RELAY)
val author = tlv.firstAsHex(TlvTypes.AUTHOR)
val kind = tlv.firstAsInt(TlvTypes.KIND.id)
return Return(Type.EVENT, hex, relay, author, kind)
}
private fun nrelay(bytes: ByteArray): Return? {
val relayUrl = Tlv.parse(bytes).firstAsString(TlvTypes.SPECIAL.id) ?: return null
return Return(Type.RELAY, relayUrl)
}
private fun naddr(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
val d = tlv.firstAsString(TlvTypes.SPECIAL.id) ?: ""
val relay = tlv.firstAsString(TlvTypes.RELAY.id)
val author = tlv.firstAsHex(TlvTypes.AUTHOR.id) ?: return null
val kind = tlv.firstAsInt(TlvTypes.KIND.id) ?: return null
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
}
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
return TlvBuilder().apply {
addHex(TlvTypes.SPECIAL, idHex)
addStringIfNotNull(TlvTypes.RELAY, relay)
addHexIfNotNull(TlvTypes.AUTHOR, author)
addIntIfNotNull(TlvTypes.KIND, kind)
}.build().toNEvent()
}
}
fun decodePublicKey(key: String): ByteArray {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray()
return if (key.startsWith("nsec")) {
KeyPair(privKey = key.bechToBytes()).pubKey
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key)
}
}
fun decodePublicKeyAsHexOrNull(key: String): HexKey? {
return try {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex
if (key.startsWith("nsec")) {
KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey()
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key).toHexKey()
}
} catch (e: Exception) {
null
}
}
fun TlvBuilder.addString(type: Nip19.TlvTypes, string: String) = addString(type.id, string)
fun TlvBuilder.addHex(type: Nip19.TlvTypes, key: HexKey) = addHex(type.id, key)
fun TlvBuilder.addInt(type: Nip19.TlvTypes, data: Int) = addInt(type.id, data)
fun TlvBuilder.addStringIfNotNull(type: Nip19.TlvTypes, data: String?) = addStringIfNotNull(type.id, data)
fun TlvBuilder.addHexIfNotNull(type: Nip19.TlvTypes, data: HexKey?) = addHexIfNotNull(type.id, data)
fun TlvBuilder.addIntIfNotNull(type: Nip19.TlvTypes, data: Int?) = addIntIfNotNull(type.id, data)
fun Tlv.firstAsInt(type: Nip19.TlvTypes) = firstAsInt(type.id)
fun Tlv.firstAsHex(type: Nip19.TlvTypes) = firstAsHex(type.id)
fun Tlv.firstAsString(type: Nip19.TlvTypes) = firstAsString(type.id)
@@ -1,69 +0,0 @@
package com.vitorpamplona.quartz.encoders
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class TlvBuilder() {
val outputStream = ByteArrayOutputStream()
private fun add(type: Byte, byteArray: ByteArray) {
outputStream.write(byteArrayOf(type, byteArray.size.toByte()))
outputStream.write(byteArray)
}
fun addString(type: Byte, string: String) = add(type, string.toByteArray(Charsets.UTF_8))
fun addHex(type: Byte, key: HexKey) = add(type, key.hexToByteArray())
fun addInt(type: Byte, data: Int) = add(type, data.to32BitByteArray())
fun addStringIfNotNull(type: Byte, data: String?) = data?.let { addString(type, it) }
fun addHexIfNotNull(type: Byte, data: HexKey?) = data?.let { addHex(type, it) }
fun addIntIfNotNull(type: Byte, data: Int?) = data?.let { addInt(type, it) }
fun build(): ByteArray {
return outputStream.toByteArray()
}
}
fun Int.to32BitByteArray(): ByteArray {
val bytes = ByteArray(4)
(0..3).forEach {
bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte()
}
return bytes
}
fun ByteArray.toInt32(): Int? {
if (size != 4) return null
return ByteBuffer.wrap(this, 0, 4).order(ByteOrder.BIG_ENDIAN).int
}
class Tlv(val data: Map<Byte, List<ByteArray>>) {
fun asInt(type: Byte) = data[type]?.mapNotNull { it.toInt32() }
fun asHex(type: Byte) = data[type]?.map { it.toHexKey().intern() }
fun asString(type: Byte) = data[type]?.map { it.toString(Charsets.UTF_8) }
fun firstAsInt(type: Byte) = data[type]?.firstOrNull()?.toInt32()
fun firstAsHex(type: Byte) = data[type]?.firstOrNull()?.toHexKey()?.intern()
fun firstAsString(type: Byte) = data[type]?.firstOrNull()?.toString(Charsets.UTF_8)
companion object {
fun parse(data: ByteArray): Tlv {
val result = mutableMapOf<Byte, MutableList<ByteArray>>()
var rest = data
while (rest.isNotEmpty()) {
val t = rest[0]
val l = rest[1].toUByte().toInt()
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
if (v.size < l) continue
if (!result.containsKey(t)) {
result[t] = mutableListOf()
}
result[t]?.add(v)
}
return Tlv(result)
}
}
}
@@ -1,70 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class AdvertisedRelayListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
override fun dTag() = fixedDTag
fun relays(): List<AdvertisedRelayInfo> {
return tags.mapNotNull {
if (it.size > 1 && it[0] == "r") {
val type = when (it.getOrNull(2)) {
"read" -> AdvertisedRelayType.READ
"write" -> AdvertisedRelayType.WRITE
else -> AdvertisedRelayType.BOTH
}
AdvertisedRelayInfo(it[1], type)
} else {
null
}
}
}
companion object {
const val kind = 10002
const val fixedDTag = ""
fun create(
list: List<AdvertisedRelayInfo>,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): AdvertisedRelayListEvent {
val tags = list.map {
if (it.type == AdvertisedRelayType.BOTH) {
listOf(it.relayUrl)
} else {
listOf(it.relayUrl, it.type.code)
}
}
val msg = ""
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)
return AdvertisedRelayListEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
@Immutable
data class AdvertisedRelayInfo(val relayUrl: String, val type: AdvertisedRelayType)
@Immutable
enum class AdvertisedRelayType(val code: String) {
BOTH(""),
READ("read"),
WRITE("write")
}
}
@@ -1,38 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import java.io.ByteArrayInputStream
@Immutable
class AppDefinitionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun appMetaData() = try {
mapper.readValue(
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
UserMetadata::class.java
)
} catch (e: Exception) {
e.printStackTrace()
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
null
}
fun supportedKinds() = tags.filter { it.size > 1 && it[0] == "k" }.mapNotNull {
runCatching { it[1].toInt() }.getOrNull()
}
fun publishedAt() = tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)
companion object {
const val kind = 31990
}
}
@@ -1,23 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class AppRecommendationEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun recommendations() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
ATag.parse(it[1], it.getOrNull(2))
}
companion object {
const val kind = 31989
}
}
@@ -1,62 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class AudioHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<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 {
mapper.readValue<List<Int>>(it)
}
companion object {
const val kind = 1808
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,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): AudioHeaderEvent {
val tags = listOfNotNull(
downloadUrl.let { listOf(DOWNLOAD_URL, it) },
streamUrl?.let { listOf(STREAM_URL, it) },
wavefront?.let { listOf(WAVEFORM, it) },
sensitiveContent?.let {
if (it) {
listOf("content-warning", "")
} else {
null
}
}
)
val content = description
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return AudioHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,62 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class AudioTrackEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
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 type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
fun price() = tags.firstOrNull { it.size > 1 && it[0] == PRICE }?.get(1)
fun cover() = tags.firstOrNull { it.size > 1 && it[0] == COVER }?.get(1)
// fun subject() = tags.firstOrNull { it.size > 1 && it[0] == SUBJECT }?.get(1)
fun media() = tags.firstOrNull { it.size > 1 && it[0] == MEDIA }?.get(1)
companion object {
const val kind = 31337
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(
type: String,
media: String,
price: String? = null,
cover: String? = null,
subject: String? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): AudioTrackEvent {
val tags = listOfNotNull(
listOf(MEDIA, media),
listOf(TYPE, type),
price?.let { listOf(PRICE, it) },
cover?.let { listOf(COVER, it) },
subject?.let { listOf(SUBJECT, it) }
)
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return AudioTrackEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
data class Participant(val key: String, val role: String?)
@@ -1,22 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class BadgeAwardEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun awardees() = taggedUsers()
fun awardDefinition() = taggedAddresses()
companion object {
const val kind = 8
}
}
@@ -1,24 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class BadgeDefinitionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1)
fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == "thumb" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
companion object {
const val kind = 30009
}
}
@@ -1,28 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class BadgeProfilesEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 30008
const val standardDTAg = "profile_badges"
}
}
@@ -1,148 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.Nip19.nip19regex
import java.util.regex.Pattern
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
val hashtagSearch = Pattern.compile("(?:\\s|\\A)#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)")
@Immutable
open class BaseTextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = taggedUsers()
open fun replyTos() = taggedEvents()
@Transient
private var citedUsersCache: Set<HexKey>? = null
@Transient
private var citedNotesCache: Set<HexKey>? = null
fun citedUsers(): Set<HexKey> {
citedUsersCache?.let { return it }
val matcher = tagSearch.matcher(content)
val returningList = mutableSetOf<String>()
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { tags[it.toInt()] }
if (tag != null && tag.size > 1 && tag[0] == "p") {
returningList.add(tag[1])
}
} catch (e: Exception) {
}
}
val matcher2 = nip19regex.matcher(content)
while (matcher2.find()) {
val uriScheme = matcher2.group(1) // nostr:
val type = matcher2.group(2) // npub1
val key = matcher2.group(3) // bech32
val additionalChars = matcher2.group(4) // additional chars
try {
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
if (parsed != null) {
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
if (tag != null && tag[0] == "p") {
returningList.add(tag[1])
}
}
} catch (e: Exception) {
Log.w("Unable to parse cited users that matched a NIP19 regex", e)
}
}
citedUsersCache = returningList
return returningList
}
fun findCitations(): Set<HexKey> {
citedNotesCache?.let { return it }
val citations = mutableSetOf<HexKey>()
// Removes citations from replies:
val matcher = tagSearch.matcher(content)
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { tags[it.toInt()] }
if (tag != null && tag.size > 1 && tag[0] == "e") {
citations.add(tag[1])
}
if (tag != null && tag.size > 1 && tag[0] == "a") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
val matcher2 = nip19regex.matcher(content)
while (matcher2.find()) {
val uriScheme = matcher2.group(1) // nostr:
val type = matcher2.group(2) // npub1
val key = matcher2.group(3) // bech32
val additionalChars = matcher2.group(4) // additional chars
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
if (parsed != null) {
try {
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
if (tag != null && tag[0] == "e") {
citations.add(tag[1])
}
if (tag != null && tag[0] == "a") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
}
citedNotesCache = citations
return citations
}
fun tagsWithoutCitations(): List<String> {
val repliesTo = replyTos()
val tagAddresses = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.kind }.map { it.toTag() }
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
val citations = findCitations()
return if (citations.isEmpty()) {
repliesTo + tagAddresses
} else {
repliesTo.filter { it !in citations }
}
}
}
fun findHashtags(content: String): List<String> {
val matcher = hashtagSearch.matcher(content)
val returningList = mutableSetOf<String>()
while (matcher.find()) {
try {
val tag = matcher.group(1)
if (tag != null && tag.isNotBlank()) {
returningList.add(tag)
}
} catch (e: Exception) {
}
}
return returningList.toList()
}
@@ -1,91 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class BookmarkListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
var decryptedContent = ""
companion object {
const val kind = 30001
fun create(
name: String = "",
events: List<String>? = null,
users: List<String>? = null,
addresses: List<ATag>? = null,
content: String,
pubKey: HexKey,
createdAt: Long = TimeUtils.now()
): BookmarkListEvent {
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
events?.forEach {
tags.add(listOf("e", it))
}
users?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
return BookmarkListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
}
fun create(
name: String = "",
events: List<String>? = null,
users: List<String>? = null,
addresses: List<ATag>? = null,
privEvents: List<String>? = null,
privUsers: List<String>? = null,
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): BookmarkListEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
events?.forEach {
tags.add(listOf("e", it))
}
users?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return BookmarkListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
fun create(
unsignedEvent: BookmarkListEvent, signature: String
): BookmarkListEvent {
return BookmarkListEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,41 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarDateSlotEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)
fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)
// ["start", "<YYYY-MM-DD>"],
// ["end", "<YYYY-MM-DD>"],
companion object {
const val kind = 31922
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarDateSlotEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarDateSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,33 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 31924
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,43 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarRSVPEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun status() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)
fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)
// ["L", "status"],
// ["l", "<accepted/declined/tentative>", "status"],
// ["L", "freebusy"],
// ["l", "<free/busy>", "freebusy"]
companion object {
const val kind = 31925
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarRSVPEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarRSVPEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,45 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarTimeSlotEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)?.toLongOrNull()
fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)?.toLongOrNull()
fun startTmz() = tags.firstOrNull { it.size > 1 && it[0] == "start_tzid" }?.get(1)?.toLongOrNull()
fun endTmz() = tags.firstOrNull { it.size > 1 && it[0] == "end_tzid" }?.get(1)?.toLongOrNull()
// ["start", "<Unix timestamp in seconds>"],
// ["end", "<Unix timestamp in seconds>"],
// ["start_tzid", "<IANA Time Zone Database identifier>"],
// ["end_tzid", "<IANA Time Zone Database identifier>"],
companion object {
const val kind = 31923
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarTimeSlotEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarTimeSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,58 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.key
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ChannelCreateEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channelInfo(): ChannelData = try {
mapper.readValue(content)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelData(null, null, null)
}
companion object {
const val kind = 40
fun create(channelInfo: ChannelData?, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ChannelCreateEvent {
val content = try {
if (channelInfo != null) {
mapper.writeValueAsString(channelInfo)
} else {
""
}
} catch (t: Throwable) {
Log.e("ChannelCreateEvent", "Couldn't parse channel information", t)
""
}
val pubKey = keyPair.pubKey.toHexKey()
val tags = emptyList<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.pubKey)
return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: ChannelCreateEvent, signature: String): ChannelCreateEvent {
return ChannelCreateEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
@Immutable
data class ChannelData(val name: String?, val about: String?, val picture: String?)
}
@@ -1,41 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ChannelHideMessageEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 43
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags =
messagesToHide?.map {
listOf("e", it)
} ?: emptyList()
val id = generateId(pubKey, createdAt, kind, tags, reason)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, reason, sig.toHexKey())
}
}
}
@@ -1,82 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ChannelMessageEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
override fun replyTos() = tags.filter { it.firstOrNull() == "e" && it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 42
fun create(
message: String,
channel: String,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String?,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?,
geohash: String? = null
): ChannelMessageEvent {
val content = message
val tags = mutableListOf(
listOf("e", channel, "", "root")
)
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: ChannelMessageEvent, signature: String
): ChannelMessageEvent {
return ChannelMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
interface IsInPublicChatChannel {
fun channel(): String?
}
@@ -1,52 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ChannelMetadataEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun channelInfo() =
try {
mapper.readValue(content, ChannelCreateEvent.ChannelData::class.java)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelCreateEvent.ChannelData(null, null, null)
}
companion object {
const val kind = 41
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ChannelMetadataEvent {
val content =
if (newChannelInfo != null) {
mapper.writeValueAsString(newChannelInfo)
} else {
""
}
val pubKey = keyPair.pubKey.toHexKey()
val tags = listOf(listOf("e", originalChannelIdHex, "", "root"))
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: ChannelMetadataEvent, signature: String): ChannelMetadataEvent {
return ChannelMetadataEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,42 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ChannelMuteUserEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 44
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent {
val content = reason
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags =
usersToMute?.map {
listOf("p", it)
} ?: emptyList()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,109 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableSet
@Immutable
class ChatMessageEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable {
/**
* Recepients intended to receive this conversation
*/
fun recipientsPubKey() = tags.mapNotNull {
if (it.size > 1 && it[0] == "p") it[1] else null
}
fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun talkingWith(oneSideHex: String): Set<HexKey> {
val listedPubKeys = recipientsPubKey()
val result = if (pubKey == oneSideHex) {
listedPubKeys.toSet().minus(oneSideHex)
} else {
listedPubKeys.plus(pubKey).toSet().minus(oneSideHex)
}
if (result.isEmpty()) {
// talking to myself
return setOf(pubKey)
}
return result
}
override fun chatroomKey(toRemove: String): ChatroomKey {
return ChatroomKey(talkingWith(toRemove).toImmutableSet())
}
companion object {
const val kind = 14
fun create(
msg: String,
to: List<String>? = null,
subject: String? = null,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): ChatMessageEvent {
val content = msg
val tags = mutableListOf<List<String>>()
to?.forEach {
tags.add(listOf("p", it))
}
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it, "", "mention"))
}
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
subject?.let {
tags.add(listOf("subject", it))
}
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, ClassifiedsEvent.kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return ChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
interface ChatroomKeyable {
fun chatroomKey(toRemove: HexKey): ChatroomKey
}
@Stable
data class ChatroomKey(
val users: ImmutableSet<HexKey>
)
@@ -1,74 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ClassifiedsEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let {
Price(it[1], it.getOrNull(2), it.getOrNull(3))
}
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun publishedAt() = try {
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
} catch (_: Exception) {
null
}
companion object {
const val kind = 30402
fun create(
dTag: String,
title: String?,
image: String?,
summary: String?,
price: Price?,
location: String?,
publishedAt: Long?,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): ClassifiedsEvent {
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", dTag))
title?.let { tags.add(listOf("title", it)) }
image?.let { tags.add(listOf("image", it)) }
summary?.let { tags.add(listOf("summary", it)) }
price?.let {
if (it.frequency != null && it.currency != null) {
tags.add(listOf("price", it.amount, it.currency, it.frequency))
} else if (it.currency != null) {
tags.add(listOf("price", it.amount, it.currency))
} else {
tags.add(listOf("price", it.amount))
}
}
location?.let { tags.add(listOf("location", it)) }
publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) }
title?.let { tags.add(listOf("title", it)) }
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
data class Price(val amount: String, val currency: String?, val frequency: String?)
@@ -1,39 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CommunityDefinitionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun rules() = tags.firstOrNull { it.size > 1 && it[0] == "rules" }?.get(1)
fun moderators() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
companion object {
const val kind = 34550
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CommunityDefinitionEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CommunityDefinitionEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,63 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CommunityPostApprovalEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun containedPost(): Event? = try {
content.ifBlank { null }?.let {
fromJson(it)
}
} catch (e: Exception) {
Log.w("CommunityPostEvent", "Failed to Parse Community Approval Contained Post of $id with $content")
null
}
fun communities() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
val aTag = ATag.parse(it[1], it.getOrNull(2))
if (aTag?.kind == CommunityDefinitionEvent.kind) {
aTag
} else {
null
}
}
fun approvedEvents() = tags.filter {
it.size > 1 && (it[0] == "e" || (it[0] == "a" && ATag.parse(it[1], null)?.kind != CommunityDefinitionEvent.kind))
}.map {
it[1]
}
companion object {
const val kind = 4550
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
val content = approvedPost.toJson()
val communities = listOf("a", community.address().toTag())
val replyToPost = listOf("e", approvedPost.id())
val replyToAuthor = listOf("p", approvedPost.pubKey())
val kind = listOf("k", "${approvedPost.kind()}")
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(communities, replyToPost, replyToAuthor, kind)
val id = generateId(pubKey, createdAt, GenericRepostEvent.kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,355 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.decodePublicKey
@Immutable
data class Contact(val pubKeyHex: String, val relayUri: String?)
@Stable
class ContactListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
// This function is only used by the user logged in
// But it is used all the time.
@delegate:Transient
val verifiedFollowKeySet: Set<HexKey> by lazy {
tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull {
try {
decodePublicKey(it[1]).toHexKey()
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
}.toSet()
}
@delegate:Transient
val verifiedFollowTagSet: Set<String> by lazy {
unverifiedFollowTagSet().map { it.lowercase() }.toSet()
}
@delegate:Transient
val verifiedFollowGeohashSet: Set<String> by lazy {
unverifiedFollowGeohashSet().map { it.lowercase() }.toSet()
}
@delegate:Transient
val verifiedFollowCommunitySet: Set<String> by lazy {
unverifiedFollowAddressSet().toSet()
}
@delegate:Transient
val verifiedFollowKeySetAndMe: Set<HexKey> by lazy {
verifiedFollowKeySet + pubKey
}
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowGeohashSet() = tags.filter { it[0] == "g" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) }
fun follows() = tags.filter { it[0] == "p" }.mapNotNull {
try {
Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2))
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
}
fun followsTags() = tags.filter { it[0] == "t" }.mapNotNull {
it.getOrNull(2)
}
fun relays(): Map<String, ReadWrite>? = try {
if (content.isNotEmpty()) {
mapper.readValue<Map<String, ReadWrite>>(content)
} else {
null
}
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse content as relay lists: $content", e)
null
}
companion object {
const val kind = 3
fun createFromScratch(
followUsers: List<Contact>,
followTags: List<String>,
followGeohashes: List<String>,
followCommunities: List<ATag>,
followEvents: List<String>,
relayUse: Map<String, ReadWrite>?,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): ContactListEvent {
val content = if (relayUse != null) {
mapper.writeValueAsString(relayUse)
} else {
""
}
val tags = followUsers.map {
if (it.relayUri != null) {
listOf("p", it.pubKeyHex, it.relayUri)
} else {
listOf("p", it.pubKeyHex)
}
} + followTags.map {
listOf("t", it)
} + followEvents.map {
listOf("e", it)
} + followCommunities.map {
if (it.relay != null) {
listOf("a", it.toTag(), it.relay)
} else {
listOf("a", it.toTag())
}
} + followGeohashes.map {
listOf("g", it)
}
return create(
content = content,
tags = tags,
keyPair = keyPair,
createdAt = createdAt
)
}
fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
keyPair = keyPair,
createdAt = createdAt
)
}
fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
keyPair = keyPair,
createdAt = createdAt
)
}
fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("t", hashtag)),
keyPair = keyPair,
createdAt = createdAt
)
}
fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) },
keyPair = keyPair,
createdAt = createdAt
)
}
fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("g", hashtag)),
keyPair = keyPair,
createdAt = createdAt
)
}
fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
keyPair = keyPair,
createdAt = createdAt
)
}
fun followEvent(earlierVersion: ContactListEvent, idHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("e", idHex)),
keyPair = keyPair,
createdAt = createdAt
)
}
fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedEvent(idHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex },
keyPair = keyPair,
createdAt = createdAt
)
}
fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOfNotNull("a", aTag.toTag(), aTag.relay)),
keyPair = keyPair,
createdAt = createdAt
)
}
fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() },
keyPair = keyPair,
createdAt = createdAt
)
}
fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map<String, ReadWrite>?, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
val content = if (relayUse != null) {
mapper.writeValueAsString(relayUse)
} else {
""
}
return create(
content = content,
tags = earlierVersion.tags,
keyPair = keyPair,
createdAt = createdAt
)
}
fun create(
unsignedEvent: Event,
signature: String
): ContactListEvent {
return ContactListEvent(
unsignedEvent.id,
unsignedEvent.pubKey,
unsignedEvent.createdAt,
unsignedEvent.tags,
unsignedEvent.content,
signature
)
}
fun create(
content: String,
tags: List<List<String>>,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): ContactListEvent {
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
}
data class ReadWrite(val read: Boolean, val write: Boolean)
}
@Stable
class UserMetadata {
var name: String? = null
var username: String? = null
var display_name: String? = null
var displayName: String? = null
var picture: String? = null
var banner: String? = null
var website: String? = null
var about: String? = null
var nip05: String? = null
var nip05Verified: Boolean = false
var nip05LastVerificationTime: Long? = 0
var domain: String? = null
var lud06: String? = null
var lud16: String? = null
var twitter: String? = null
var updatedMetadataAt: Long = 0
var latestMetadata: MetadataEvent? = null
var tags: ImmutableListOfLists<String>? = null
fun anyName(): String? {
return display_name ?: displayName ?: name ?: username
}
fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
.any { it.contains(prefix, true) }
}
fun lnAddress(): String? {
return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null }
}
fun bestUsername(): String? {
return name?.ifBlank { null } ?: username?.ifBlank { null }
}
fun bestDisplayName(): String? {
return displayName?.ifBlank { null } ?: display_name?.ifBlank { null }
}
fun nip05(): String? {
return nip05?.ifBlank { null }
}
fun profilePicture(): String? {
if (picture.isNullOrBlank()) picture = null
return picture
}
}
@Stable
data class ImmutableListOfLists<T>(val lists: List<List<T>> = emptyList())
fun List<List<String>>.toImmutableListOfLists(): ImmutableListOfLists<String> {
return ImmutableListOfLists(this)
}
@@ -1,37 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class DeletionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun deleteEvents() = tags.map { it[1] }
companion object {
const val kind = 5
fun create(deleteEvents: List<String>, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): DeletionEvent {
val content = ""
val pubKey = keyPair.pubKey.toHexKey()
val tags = deleteEvents.map { listOf("e", it) }
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: DeletionEvent, signature: String): DeletionEvent {
return DeletionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,56 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class EmojiPackEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 30030
fun create(
name: String = "",
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): EmojiPackEvent {
val content = ""
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
}
@Immutable
data class EmojiUrl(val code: String, val url: String) {
fun encode(): String {
return ":$code:$url"
}
companion object {
fun decode(encodedEmojiSetup: String): EmojiUrl? {
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
return if (emojiParts.size > 2) {
EmojiUrl(emojiParts[1], emojiParts[2])
} else {
null
}
}
}
}
@@ -1,47 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class EmojiPackSelectionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 10030
fun create(
listOfEmojiPacks: List<ATag>?,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): EmojiPackSelectionEvent {
val msg = ""
val pubKey = keyPair.pubKey.toHexKey()
val tags = mutableListOf<List<String>>()
listOfEmojiPacks?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return EmojiPackSelectionEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: EmojiPackSelectionEvent, signature: String
): EmojiPackSelectionEvent {
return EmojiPackSelectionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,423 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
import java.util.*
@Immutable
open class Event(
val id: HexKey,
@JsonProperty("pubkey")
val pubKey: HexKey,
@JsonProperty("created_at")
val createdAt: Long,
val kind: Int,
val tags: List<List<String>>,
val content: String,
val sig: HexKey
) : EventInterface {
override fun countMemory(): Long {
return 12L +
id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() +
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
content.bytesUsedInMemory() +
sig.bytesUsedInMemory()
}
override fun id(): HexKey = id
override fun pubKey(): HexKey = pubKey
override fun createdAt(): Long = createdAt
override fun kind(): Int = kind
override fun tags(): List<List<String>> = tags
override fun content(): String = content
override fun sig(): HexKey = sig
override fun toJson(): String = mapper.writeValueAsString(toJsonObject())
fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" }
override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] }
override fun firstTaggedEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "a" }?.let {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
override fun isSensitive() = tags.any {
(it.size > 0 && it[0].equals("content-warning", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nude", true))
}
override fun subject() = tags.firstOrNull() { it.size > 1 && it[0] == "subject" }?.get(1)
override fun zapraiserAmount() = tags.firstOrNull() {
(it.size > 1 && it[0] == "zapraiser")
}?.get(1)?.toLongOrNull()
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
override fun geohashes() = tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] }
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
override fun isTaggedUsers(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "p" && it[1] in idHexes }
override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex }
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes }
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
override fun isTaggedGeoHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "g" && it[1].startsWith(hashtag, true) }
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
override fun isTaggedGeoHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "g" && it[1].lowercase() in hashtags }
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
override fun firstIsTaggedAddressableNote(addressableNotes: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1)
override fun isTaggedAddressableKind(kind: Int): Boolean {
val kindStr = kind.toString()
return tags.any { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
}
override fun expiration() = try {
tags.firstOrNull { it.size > 1 && it[0] == "expiration" }?.get(1)?.toLongOrNull()
} catch (_: Exception) {
null
}
override fun isExpired() = (expiration() ?: Long.MAX_VALUE) < TimeUtils.now()
override fun getTagOfAddressableKind(kind: Int): ATag? {
val kindStr = kind.toString()
val aTag = tags
.firstOrNull { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
?.getOrNull(1)
?: return null
return ATag.parse(aTag, null)
}
override fun getPoWRank(): Int {
var rank = 0
for (i in 0..id.length) {
if (id[i] == '0') {
rank += 4
} else if (id[i] in '4'..'7') {
rank += 1
break
} else if (id[i] in '2'..'3') {
rank += 2
break
} else if (id[i] == '1') {
rank += 3
break
} else {
break
}
}
return rank
}
override fun getGeoHash(): String? {
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
}
override fun getReward(): BigDecimal? {
return try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
} catch (e: Exception) {
null
}
}
open fun toNIP19(): String {
return if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr()
} else {
Nip19.createNEvent(id, pubKey, kind, null)
}
}
fun toNostrUri(): String {
return "nostr:${toNIP19()}"
}
fun hasCorrectIDHash() = id.equals(generateId())
fun hasVerifedSignature() = 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.
*/
override fun checkSignature() {
if (!hasCorrectIDHash()) {
throw Exception(
"""|Unexpected ID.
| Event: ${toJson()}
| Actual ID: $id
| Generated: ${generateId()}
""".trimIndent()
)
}
if (!hasVerifedSignature()) {
throw Exception("""Bad signature!""")
}
}
override fun hasValidSignature(): Boolean {
return try {
hasCorrectIDHash() && hasVerifedSignature()
} catch (e: Exception) {
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
false
}
}
fun makeJsonForId(): String {
return makeJsonForId(pubKey, createdAt, kind, tags, content)
}
private fun generateId(): String {
return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
}
private class EventDeserializer : StdDeserializer<Event>(Event::class.java) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Event {
return fromJson(jp.codec.readTree(jp))
}
}
private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Gossip {
val jsonObject: JsonNode = jp.codec.readTree(jp)
return Gossip(
id = jsonObject.get("id")?.asText()?.intern(),
pubKey = jsonObject.get("pubkey")?.asText()?.intern(),
createdAt = jsonObject.get("created_at")?.asLong(),
kind = jsonObject.get("kind")?.asInt(),
tags = jsonObject.get("tags")?.map {
it.mapNotNull { s -> if (s?.isNull ?: true) null else s.asText().intern() }
},
content = jsonObject.get("content")?.asText()
)
}
}
private class EventSerializer: StdSerializer<Event>(Event::class.java) {
override fun serialize(event: Event, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("id", event.id)
gen.writeStringField("pubkey", event.pubKey)
gen.writeNumberField("created_at", event.createdAt)
gen.writeNumberField("kind", event.kind)
gen.writeArrayFieldStart("tags")
event.tags.forEach { tag ->
gen.writeArray(tag.toTypedArray(), 0, tag.size)
}
gen.writeEndArray()
gen.writeStringField("content", event.content)
gen.writeStringField("sig", event.sig)
gen.writeEndObject()
}
}
private class GossipSerializer: StdSerializer<Gossip>(Gossip::class.java) {
override fun serialize(event: Gossip, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
event.id?.let { gen.writeStringField("id", it) }
event.pubKey?.let { gen.writeStringField("pubkey", it) }
event.createdAt?.let { gen.writeNumberField("created_at", it) }
event.kind?.let { gen.writeNumberField("kind", it) }
event.tags?.let {
gen.writeArrayFieldStart("tags")
event.tags.forEach { tag ->
gen.writeArray(tag.toTypedArray(), 0, tag.size)
}
gen.writeEndArray()
}
event.content?.let { gen.writeStringField("content", it) }
gen.writeEndObject()
}
}
fun toJsonObject(): JsonNode {
val factory = mapper.nodeFactory
return factory.objectNode().apply {
put("id", id)
put("pubkey", pubKey)
put("created_at", createdAt)
put("kind", kind)
put(
"tags",
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply {
tag.forEach { add(it) }
}
)
}
}
)
put("content", content)
put("sig", sig)
}
}
companion object {
val mapper = jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(SimpleModule()
.addSerializer(Event::class.java, EventSerializer())
.addDeserializer(Event::class.java, EventDeserializer())
.addSerializer(Gossip::class.java, GossipSerializer())
.addDeserializer(Gossip::class.java, GossipDeserializer())
.addDeserializer(Response::class.java, ResponseDeserializer())
.addDeserializer(Request::class.java, RequestDeserializer())
)
fun fromJson(jsonObject: JsonNode): Event {
return EventFactory.create(
id = jsonObject.get("id").asText().intern(),
pubKey = jsonObject.get("pubkey").asText().intern(),
createdAt = jsonObject.get("created_at").asLong(),
kind = jsonObject.get("kind").asInt(),
tags = jsonObject.get("tags").map {
it.mapNotNull { s -> if (s.isNull) null else s.asText().intern() }
},
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig").asText()
)
}
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
fun toJson(event: Event): String = mapper.writeValueAsString(event)
fun makeJsonForId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List<List<String>>, content: String): String {
val factory = mapper.nodeFactory
val rawEvent = factory.arrayNode(6).apply {
add(0)
add(pubKey)
add(createdAt)
add(kind)
add(
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply {
tag.forEach { add(it) }
}
)
}
}
)
add(content)
}
return mapper.writeValueAsString(rawEvent)
}
fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List<List<String>>, content: String): ByteArray {
return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
}
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey).toHexKey()
return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig)
}
}
}
@Immutable
open class WrappedEvent(
id: HexKey,
@JsonProperty("pubkey")
pubKey: HexKey,
@JsonProperty("created_at")
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
var host: Event? = null // host event to broadcast when needed
}
@Immutable
interface AddressableEvent {
fun dTag(): String
fun address(): ATag
}
@Immutable
open class BaseAddressableEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
}
fun String.bytesUsedInMemory(): Int {
return (8 * ((((this.length) * 2) + 45) / 8))
}
@@ -1,82 +0,0 @@
package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.toHexKey
class EventFactory {
companion object {
fun create(
id: String,
pubKey: String,
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: String
) = when (kind) {
AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AudioHeaderEvent.kind -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.kind -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.kind -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.kind -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent.kind -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarRSVPEvent.kind -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
ChatMessageEvent.kind -> {
if (id.isBlank()) {
val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey()
ChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
} else {
ChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
}
}
ClassifiedsEvent.kind -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
SealedGossipEvent.kind -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.kind -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPrivateEvent.kind -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig)
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
NNSEvent.kind -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.kind -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig)
RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
StatusEvent.kind -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
else -> Event(id, pubKey, createdAt, kind, tags, content, sig)
}
}
}
@@ -1,79 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import java.math.BigDecimal
@Immutable
interface EventInterface {
fun countMemory(): Long
fun id(): HexKey
fun pubKey(): HexKey
fun createdAt(): Long
fun kind(): Int
fun tags(): List<List<String>>
fun content(): String
fun sig(): HexKey
fun toJson(): String
fun checkSignature()
fun hasValidSignature(): Boolean
fun isTaggedUser(idHex: String): Boolean
fun isTaggedUsers(idHex: Set<String>): Boolean
fun isTaggedEvent(idHex: String): Boolean
fun isTaggedAddressableNote(idHex: String): Boolean
fun isTaggedAddressableNotes(idHexes: Set<String>): Boolean
fun isTaggedHash(hashtag: String): Boolean
fun isTaggedGeoHash(hashtag: String): Boolean
fun isTaggedHashes(hashtags: Set<String>): Boolean
fun isTaggedGeoHashes(hashtags: Set<String>): Boolean
fun firstIsTaggedHashes(hashtags: Set<String>): String?
fun firstIsTaggedAddressableNote(addressableNotes: Set<String>): String?
fun isTaggedAddressableKind(kind: Int): Boolean
fun getTagOfAddressableKind(kind: Int): ATag?
fun expiration(): Long?
fun hashtags(): List<String>
fun geohashes(): List<String>
fun getReward(): BigDecimal?
fun getPoWRank(): Int
fun getGeoHash(): String?
fun zapAddress(): String?
fun isSensitive(): Boolean
fun subject(): String?
fun zapraiserAmount(): Long?
fun taggedAddresses(): List<ATag>
fun taggedUsers(): List<HexKey>
fun taggedEvents(): List<HexKey>
fun taggedUrls(): List<String>
fun firstTaggedAddress(): ATag?
fun firstTaggedUser(): HexKey?
fun firstTaggedEvent(): HexKey?
fun firstTaggedUrl(): String?
fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean
fun isExpired(): Boolean
}
@@ -1,88 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class FileHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
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 encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
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 dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
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 hasUrl() = tags.any { it.size > 1 && it[0] == URL }
companion object {
const val kind = 1063
private const val URL = "url"
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"
fun create(
url: String,
mimeType: String? = null,
description: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): FileHeaderEvent {
val tags = listOfNotNull(
listOf(URL, url),
mimeType?.let { listOf(MIME_TYPE, mimeType) },
hash?.let { listOf(HASH, it) },
size?.let { listOf(FILE_SIZE, it) },
dimensions?.let { listOf(DIMENSION, it) },
blurhash?.let { listOf(BLUR_HASH, it) },
magnetURI?.let { listOf(MAGNET_URI, it) },
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
sensitiveContent?.let {
if (it) {
listOf("content-warning", "")
} else {
null
}
}
)
val content = description ?: ""
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return FileHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
}
}
data class AESGCM(val key: String, val nonce: String)
@@ -1,75 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
import java.util.Base64
@Immutable
class FileStorageEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
fun decryptKey() = tags.firstOrNull { it.size > 2 && it[0] == DECRYPT }?.let { AESGCM(it[1], it[2]) }
fun decode(): ByteArray? {
return try {
Base64.getDecoder().decode(content)
} catch (e: Exception) {
Log.e("FileStorageEvent", "Unable to decode base 64 ${e.message} $content")
null
}
}
companion object {
const val kind = 1064
private const val TYPE = "type"
private const val DECRYPT = "decrypt"
fun encode(bytes: ByteArray): String {
return Base64.getEncoder().encodeToString(bytes)
}
fun create(
mimeType: String,
data: ByteArray,
pubKey: HexKey,
createdAt: Long = TimeUtils.now()
): FileStorageEvent {
val tags = listOfNotNull(
listOf(TYPE, mimeType)
)
val content = encode(data)
val id = generateId(pubKey, createdAt, kind, tags, content)
return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
}
fun create(
mimeType: String,
data: ByteArray,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): FileStorageEvent {
val tags = listOfNotNull(
listOf(TYPE, mimeType)
)
val content = encode(data)
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,122 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class FileStorageHeaderEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<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 encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
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 dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
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)
companion object {
const val kind = 1065
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"
fun create(
storageEvent: FileStorageEvent,
mimeType: String? = null,
description: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
pubKey: HexKey,
createdAt: Long = TimeUtils.now()
): FileStorageHeaderEvent {
val tags = listOfNotNull(
listOf("e", storageEvent.id),
mimeType?.let { listOf(MIME_TYPE, mimeType) },
hash?.let { listOf(HASH, it) },
size?.let { listOf(FILE_SIZE, it) },
dimensions?.let { listOf(DIMENSION, it) },
blurhash?.let { listOf(BLUR_HASH, it) },
magnetURI?.let { listOf(MAGNET_URI, it) },
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
sensitiveContent?.let {
if (it) {
listOf("content-warning", "")
} else {
null
}
}
)
val content = description ?: ""
val id = generateId(pubKey, createdAt, kind, tags, content)
return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
}
fun create(
storageEvent: FileStorageEvent,
mimeType: String? = null,
description: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): FileStorageHeaderEvent {
val tags = listOfNotNull(
listOf("e", storageEvent.id),
mimeType?.let { listOf(MIME_TYPE, mimeType) },
hash?.let { listOf(HASH, it) },
size?.let { listOf(FILE_SIZE, it) },
dimensions?.let { listOf(DIMENSION, it) },
blurhash?.let { listOf(BLUR_HASH, it) },
magnetURI?.let { listOf(MAGNET_URI, it) },
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
sensitiveContent?.let {
if (it) {
listOf("content-warning", "")
} else {
null
}
}
)
val content = description ?: ""
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,139 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
abstract class GeneralListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun category() = dTag()
fun bookmarkedPosts() = taggedEvents()
fun bookmarkedPeople() = taggedUsers()
fun plainContent(privKey: ByteArray): String? {
if (content.isBlank()) return null
return try {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray())
return CryptoUtils.decryptNIP04(content, sharedSecret)
} catch (e: Exception) {
Log.w("GeneralList", "Error decrypting the message ${e.message} for ${dTag()}")
null
}
}
@Transient
private var privateTagsCache: List<List<String>>? = null
fun privateTags(privKey: ByteArray): List<List<String>>? {
if (privateTagsCache != null) {
return privateTagsCache
}
privateTagsCache = try {
plainContent(privKey)?.let { mapper.readValue<List<List<String>>>(it) }
} catch (e: Throwable) {
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
null
}
return privateTagsCache
}
fun privateTags(content: String): List<List<String>>? {
if (privateTagsCache != null) {
return privateTagsCache
}
privateTagsCache = try {
content.let { mapper.readValue<List<List<String>>>(it) }
} catch (e: Throwable) {
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
null
}
return privateTagsCache
}
fun privateTagsOrEmpty(privKey: ByteArray): List<List<String>> {
return privateTags(privKey) ?: emptyList()
}
fun privateTagsOrEmpty(content: String): List<List<String>> {
return privateTags(content) ?: emptyList()
}
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
fun privateTaggedUsers(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
fun privateHashtags(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
fun privateGeohashes(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
fun privateTaggedEvents(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun privateTaggedAddresses(content: String) = privateTags(content)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
fun createPrivateTags(
privEvents: List<String>? = null,
privUsers: List<String>? = null,
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
pubKey: ByteArray
): String {
val privTags = mutableListOf<List<String>>()
privEvents?.forEach {
privTags.add(listOf("e", it))
}
privUsers?.forEach {
privTags.add(listOf("p", it))
}
privAddresses?.forEach {
privTags.add(listOf("a", it.toTag()))
}
val msg = mapper.writeValueAsString(privTags)
return CryptoUtils.encryptNIP04(
msg,
privateKey,
pubKey
)
}
fun encryptTags(
privateTags: List<List<String>>? = null,
privateKey: ByteArray
): String {
return CryptoUtils.encryptNIP04(
msg = mapper.writeValueAsString(privateTags),
privateKey = privateKey,
pubKey = CryptoUtils.pubkeyCreate(privateKey)
)
}
}
}
@@ -1,56 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class GenericRepostEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun containedPost() = try {
fromJson(content)
} catch (e: Exception) {
null
}
companion object {
const val kind = 16
fun create(boostedPost: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
val content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id())
val replyToAuthor = listOf("p", boostedPost.pubKey())
val pubKey = keyPair.pubKey.toHexKey()
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
if (boostedPost is AddressableEvent) {
tags = tags + listOf(listOf("a", boostedPost.address().toTag()))
}
tags = tags + listOf(listOf("k", "${boostedPost.kind()}"))
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: GenericRepostEvent, signature: String): GenericRepostEvent {
return GenericRepostEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,114 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.Nip44Version
import com.vitorpamplona.quartz.crypto.decodeNIP44
import com.vitorpamplona.quartz.crypto.encodeNIP44
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class GiftWrapEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
fun cachedGift(privKey: ByteArray): Event? {
val hex = privKey.toHexKey()
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val myInnerEvent = unwrap(privKey = privKey)
if (myInnerEvent is WrappedEvent) {
myInnerEvent.host = this
}
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
return myInnerEvent
}
fun cachedGift(pubKey: ByteArray, decryptedContent: String): Event? {
val hex = pubKey.toHexKey()
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val myInnerEvent = unwrap(decryptedContent)
if (myInnerEvent is WrappedEvent) {
myInnerEvent.host = this
}
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
return myInnerEvent
}
fun unwrap(privKey: ByteArray) = try {
plainContent(privKey)?.let { fromJson(it) }
} catch (e: Exception) {
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
null
}
fun unwrap(decryptedContent: String) = try {
plainContent(decryptedContent)?.let { fromJson(it) }
} catch (e: Exception) {
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
null
}
private fun plainContent(privKey: ByteArray): String? {
if (content.isEmpty()) return null
return try {
val toDecrypt = decodeNIP44(content) ?: return null
return when (toDecrypt.v) {
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray())
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray())
else -> null
}
} catch (e: Exception) {
Log.w("GeneralList", "Error decrypting the message ${e.message}")
null
}
}
private fun plainContent(decryptedContent: String): String? {
if (decryptedContent.isEmpty()) return null
return decryptedContent
}
fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
companion object {
const val kind = 1059
fun create(
event: Event,
recipientPubKey: HexKey,
createdAt: Long = TimeUtils.randomWithinAWeek()
): GiftWrapEvent {
val privateKey = CryptoUtils.privkeyCreate() // GiftWrap is always a random key
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, recipientPubKey.hexToByteArray())
val content = encodeNIP44(
CryptoUtils.encryptNIP44(
toJson(event),
sharedSecret
)
)
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(listOf("p", recipientPubKey))
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return GiftWrapEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,53 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class HTTPAuthorizationEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 27235
fun create(
url: String,
method: String,
body: String? = null,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): HTTPAuthorizationEvent {
var hash = ""
body?.let {
hash = CryptoUtils.sha256(it.toByteArray()).toHexKey()
}
val tags = listOfNotNull(
listOf("u", url),
listOf("method", method),
listOf("payload", hash)
)
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return HTTPAuthorizationEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: HTTPAuthorizationEvent, signature: String
): HTTPAuthorizationEvent {
return HTTPAuthorizationEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,40 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class HighlightEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun inUrl() = taggedUrls().firstOrNull()
fun author() = taggedUsers().firstOrNull()
fun quote() = content
fun inPost() = taggedAddresses().firstOrNull()
companion object {
const val kind = 9802
fun create(
msg: String,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): HighlightEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)
return HighlightEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
}
@@ -1,94 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class LiveActivitiesChatMessageEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
private fun innerActivity() = tags.firstOrNull {
it.size > 3 && it[0] == "a" && it[3] == "root"
} ?: tags.firstOrNull {
it.size > 1 && it[0] == "a"
}
private fun activityHex() = innerActivity()?.let {
it.getOrNull(1)
}
fun activity() = innerActivity()?.let {
if (it.size > 1) {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
} else {
null
}
}
override fun replyTos() = taggedEvents().minus(activityHex() ?: "")
companion object {
const val kind = 1311
fun create(
message: String,
activity: ATag,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String?,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?,
geohash: String? = null
): LiveActivitiesChatMessageEvent {
val content = message
val tags = mutableListOf(
listOf("a", activity.toTag(), "", "root")
)
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return LiveActivitiesChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: LiveActivitiesChatMessageEvent, signature: String
): LiveActivitiesChatMessageEvent {
return LiveActivitiesChatMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,61 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class LiveActivitiesEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun streaming() = tags.firstOrNull { it.size > 1 && it[0] == "streaming" }?.get(1)
fun starts() = tags.firstOrNull { it.size > 1 && it[0] == "starts" }?.get(1)?.toLongOrNull()
fun ends() = tags.firstOrNull { it.size > 1 && it[0] == "ends" }?.get(1)
fun status() = checkStatus(tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1))
fun currentParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "current_participants" }?.get(1)
fun totalParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "total_participants" }?.get(1)
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
fun checkStatus(eventStatus: String?): String? {
return if (eventStatus == STATUS_LIVE && createdAt < TimeUtils.eightHoursAgo()) {
STATUS_ENDED
} else {
eventStatus
}
}
fun participantsIntersect(keySet: Set<String>): Boolean {
return tags.any { it.size > 1 && it[0] == "p" && it[1] in keySet }
}
companion object {
const val kind = 30311
const val STATUS_LIVE = "live"
const val STATUS_PLANNED = "planned"
const val STATUS_ENDED = "ended"
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): LiveActivitiesEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return LiveActivitiesEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,74 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
@Immutable
class LnZapEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) {
// This event is also kept in LocalCache (same object)
@Transient val zapRequest: LnZapRequestEvent?
override fun containedPost(): LnZapRequestEvent? = try {
description()?.ifBlank { null }?.let {
fromJson(it)
} as? LnZapRequestEvent
} catch (e: Exception) {
Log.w("LnZapEvent", "Failed to Parse Contained Post ${description()}", e)
null
}
init {
zapRequest = containedPost()
}
override fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun zappedPollOption(): Int? = try {
zapRequest?.tags?.firstOrNull { it.size > 1 && it[0] == POLL_OPTION }?.get(1)?.toInt()
} catch (e: Exception) {
Log.e("LnZapEvent", "ZappedPollOption failed to parse", e)
null
}
override fun zappedRequestAuthor(): String? = zapRequest?.pubKey()
override fun amount() = amount
// Keeps this as a field because it's a heavier function used everywhere.
val amount by lazy {
try {
lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
} catch (e: Exception) {
Log.e("LnZapEvent", "Failed to Parse LnInvoice ${description()}", e)
null
}
}
override fun content(): String {
return content
}
fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1)
private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
companion object {
const val kind = 9735
}
enum class ZapType() {
PUBLIC,
PRIVATE,
ANONYMOUS,
NONZAP
}
}
@@ -1,20 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import java.math.BigDecimal
@Immutable
interface LnZapEventInterface : EventInterface {
fun zappedPost(): List<String>
fun zappedPollOption(): Int?
fun zappedAuthor(): List<String>
fun zappedRequestAuthor(): String?
fun amount(): BigDecimal?
fun containedPost(): Event?
}
@@ -1,107 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class LnZapPaymentRequestEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
// Once one of an app user decrypts the payment, all users else can see it.
@Transient
private var lnInvoice: String? = null
fun walletServicePubKey() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
fun lnInvoice(privKey: ByteArray, pubkey: ByteArray): String? {
if (lnInvoice != null) {
return lnInvoice
}
return try {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubkey)
val jsonText = CryptoUtils.decryptNIP04(content, sharedSecret)
val payInvoiceMethod = mapper.readValue(jsonText, Request::class.java)
lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice
return lnInvoice
} catch (e: Exception) {
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
null
}
}
companion object {
const val kind = 23194
fun create(
lnInvoice: String,
walletServicePubkey: String,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): LnZapPaymentRequestEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val serializedRequest = mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice))
val content = CryptoUtils.encryptNIP04(
serializedRequest,
privateKey,
walletServicePubkey.hexToByteArray()
)
val tags = mutableListOf<List<String>>()
tags.add(listOf("p", walletServicePubkey))
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return LnZapPaymentRequestEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
}
// REQUEST OBJECTS
abstract class Request(var method: String? = null)
// PayInvoice Call
class PayInvoiceParams(var invoice: String? = null)
class PayInvoiceMethod(var params: PayInvoiceParams? = null) : Request("pay_invoice") {
companion object {
fun create(bolt11: String): PayInvoiceMethod {
return PayInvoiceMethod(PayInvoiceParams(bolt11))
}
}
}
class RequestDeserializer : StdDeserializer<Request>(Request::class.java) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Request? {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val method = jsonObject.get("method")?.asText()
if (method == "pay_invoice") {
return jp.codec.treeToValue(jsonObject, PayInvoiceMethod::class.java)
}
return null
}
}
@@ -1,132 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class LnZapPaymentResponseEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
// Once one of an app user decrypts the payment, all users else can see it.
@Transient
private var response: Response? = null
fun requestAuthor() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
fun requestId() = tags.firstOrNull() { it.size > 1 && it[0] == "e" }?.get(1)
private fun decrypt(privKey: ByteArray, pubKey: ByteArray): String? {
return try {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey)
val retVal = CryptoUtils.decryptNIP04(content, sharedSecret)
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
retVal.substring(16)
} else {
retVal
}
} catch (e: Exception) {
Log.w("PrivateDM", "Error decrypting the message ${e.message}")
null
}
}
fun response(privKey: ByteArray, pubKey: ByteArray): Response? {
if (response != null) response
return try {
if (content.isNotEmpty()) {
val decrypted = decrypt(privKey, pubKey)
response = mapper.readValue(decrypted, Response::class.java)
response
} else {
null
}
} catch (e: Exception) {
Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e)
null
}
}
companion object {
const val kind = 23195
}
}
// RESPONSE OBJECTS
abstract class Response(
@JsonProperty("result_type")
val resultType: String
)
// PayInvoice Call
class PayInvoiceSuccessResponse(val result: PayInvoiceResultParams? = null) :
Response("pay_invoice") {
class PayInvoiceResultParams(val preimage: String)
}
class PayInvoiceErrorResponse(val error: PayInvoiceErrorParams? = null) :
Response("pay_invoice") {
class PayInvoiceErrorParams(val code: ErrorType?, val message: String?)
enum class ErrorType {
@JsonProperty(value = "RATE_LIMITED")
RATE_LIMITED, // The client is sending commands too fast. It should retry in a few seconds.
@JsonProperty(value = "NOT_IMPLEMENTED")
NOT_IMPLEMENTED, // The command is not known or is intentionally not implemented.
@JsonProperty(value = "INSUFFICIENT_BALANCE")
INSUFFICIENT_BALANCE, // The wallet does not have enough funds to cover a fee reserve or the payment amount.
@JsonProperty(value = "QUOTA_EXCEEDED")
QUOTA_EXCEEDED, // The wallet has exceeded its spending quota.
@JsonProperty(value = "RESTRICTED")
RESTRICTED, // This public key is not allowed to do this operation.
@JsonProperty(value = "UNAUTHORIZED")
UNAUTHORIZED, // This public key has no wallet connected.
@JsonProperty(value = "INTERNAL")
INTERNAL, // An internal error.
@JsonProperty(value = "OTHER")
OTHER // Other error.
}
}
class ResponseDeserializer : StdDeserializer<Response>(Response::class.java) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Response? {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val resultType = jsonObject.get("result_type")?.asText()
if (resultType == "pay_invoice") {
val result = jsonObject.get("result")
val error = jsonObject.get("error")
if (result != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java)
}
if (error != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java)
}
} else {
// tries to guess
if (jsonObject.get("result")?.get("preimage") != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java)
}
if (jsonObject.get("error")?.get("code") != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java)
}
}
return null
}
}
@@ -1,43 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.utils.TimeUtils
import java.nio.charset.Charset
import java.security.SecureRandom
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@Immutable
class LnZapPrivateEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 9733
fun create(
privateKey: ByteArray,
tags: List<List<String>> = emptyList(),
content: String = "",
createdAt: Long = TimeUtils.now()
): Event {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey).toHexKey()
return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig)
}
}
}
@@ -1,239 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.*
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import java.nio.charset.Charset
import java.security.SecureRandom
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@Immutable
class LnZapRequestEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
private var privateZapEvent: Event? = null
fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
fun isPrivateZap() = tags.any { t -> t.size >= 2 && t[0] == "anon" && t[1].isNotBlank() }
fun getPrivateZapEvent(loggedInUserPrivKey: ByteArray, pubKey: HexKey): Event? {
if (privateZapEvent != null) return privateZapEvent
val anonTag = tags.firstOrNull { t -> t.size >= 2 && t[0] == "anon" }
if (anonTag != null) {
val encnote = anonTag[1]
if (encnote.isNotBlank()) {
try {
val note = decryptPrivateZapMessage(encnote, loggedInUserPrivKey, pubKey.hexToByteArray())
val decryptedEvent = fromJson(note)
if (decryptedEvent.kind == 9733) {
privateZapEvent = decryptedEvent
return privateZapEvent
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
return null
}
companion object {
const val kind = 9734
fun create(
originalNote: EventInterface,
relays: Set<String>,
privateKey: ByteArray,
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
createdAt: Long = TimeUtils.now()
): LnZapRequestEvent {
var content = message
var privkey = privateKey
var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("relays") + relays
)
if (originalNote is AddressableEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
if (pollOption != null && pollOption >= 0) {
tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString()))
}
if (zapType == LnZapEvent.ZapType.ANONYMOUS) {
tags = tags + listOf(listOf("anon", ""))
privkey = CryptoUtils.privkeyCreate()
pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt)
val noteJson = (LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message)).toJson()
val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray())
tags = tags + listOf(listOf("anon", encryptedContent))
content = "" // make sure public content is empty, as the content is encrypted
privkey = encryptionPrivateKey // sign event with generated privkey
pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privkey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun createPublic(
originalNote: EventInterface,
relays: Set<String>,
pubKey: HexKey,
pollOption: Int?,
message: String,
createdAt: Long = TimeUtils.now()
): LnZapRequestEvent {
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("relays") + relays
)
if (originalNote is AddressableEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
if (pollOption != null && pollOption >= 0) {
tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString()))
}
val id = generateId(pubKey, createdAt, kind, tags, message)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, message, "")
}
fun createAnonymous(
originalNote: EventInterface,
relays: Set<String>,
pollOption: Int?,
message: String,
createdAt: Long = TimeUtils.now()
): LnZapRequestEvent {
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("relays") + relays
)
if (originalNote is AddressableEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
if (pollOption != null && pollOption >= 0) {
tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString()))
}
tags = tags + listOf(listOf("anon", ""))
val privkey = CryptoUtils.privkeyCreate()
val pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, message)
val sig = CryptoUtils.sign(id, privkey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, message, sig.toHexKey())
}
fun create(
userHex: String,
relays: Set<String>,
privateKey: ByteArray,
message: String,
zapType: LnZapEvent.ZapType,
createdAt: Long = TimeUtils.now()
): LnZapRequestEvent {
var content = message
var privkey = privateKey
var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
if (zapType == LnZapEvent.ZapType.ANONYMOUS) {
privkey = CryptoUtils.privkeyCreate()
pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
tags = tags + listOf(listOf("anon", ""))
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt)
val noteJson = LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message).toJson()
val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray())
tags = tags + listOf(listOf("anon", encryptedContent))
content = ""
privkey = encryptionPrivateKey
pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey()
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privkey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun createEncryptionPrivateKey(privkey: String, id: String, createdAt: Long): ByteArray {
val str = privkey + id + createdAt.toString()
val strbyte = str.toByteArray(Charset.forName("utf-8"))
return CryptoUtils.sha256(strbyte)
}
private fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
val keySpec = SecretKeySpec(sharedSecret, "AES")
val ivSpec = IvParameterSpec(iv)
val utf8message = msg.toByteArray(Charset.forName("utf-8"))
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encryptedMsg = cipher.doFinal(utf8message)
val encryptedMsgBech32 = Bech32.encode("pzap", Bech32.eight2five(encryptedMsg), Bech32.Encoding.Bech32)
val ivBech32 = Bech32.encode("iv", Bech32.eight2five(iv), Bech32.Encoding.Bech32)
return encryptedMsgBech32 + "_" + ivBech32
}
private fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
if (sharedSecret.size != 16 && sharedSecret.size != 32) {
throw IllegalArgumentException("Invalid shared secret size")
}
val parts = msg.split("_")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid message format")
}
val iv = parts[1].run { Bech32.decode(this).second }
val encryptedMsg = parts.first().run { Bech32.decode(this).second }
val encryptedBytes = Bech32.five2eight(encryptedMsg, 0)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(
Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(
Bech32.five2eight(iv, 0))
)
try {
val decryptedMsgBytes = cipher.doFinal(encryptedBytes)
return String(decryptedMsgBytes)
} catch (ex: BadPaddingException) {
throw IllegalArgumentException("Bad padding: ${ex.message}")
}
}
}
}
@@ -1,50 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class LongTextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun summary() = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun publishedAt() = try {
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
} catch (_: Exception) {
null
}
companion object {
const val kind = 30023
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): LongTextNoteEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)
return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
}
@@ -1,168 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
import java.io.ByteArrayInputStream
@Stable
abstract class IdentityClaim(
val identity: String,
val proof: String
) {
abstract fun toProofUrl(): String
abstract fun platform(): String
fun platformIdentity() = "${platform()}:$identity"
companion object {
fun create(platformIdentity: String, proof: String): IdentityClaim? {
val (platform, identity) = platformIdentity.split(':')
return when (platform.lowercase()) {
GitHubIdentity.platform -> GitHubIdentity(identity, proof)
TwitterIdentity.platform -> TwitterIdentity(identity, proof)
TelegramIdentity.platform -> TelegramIdentity(identity, proof)
MastodonIdentity.platform -> MastodonIdentity(identity, proof)
else -> throw IllegalArgumentException("Platform $platform not supported")
}
}
}
}
class GitHubIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://gist.github.com/$identity/$proof"
override fun platform() = platform
companion object {
val platform = "github"
fun parseProofUrl(proofUrl: String): GitHubIdentity? {
return try {
if (proofUrl.isBlank()) return null
val path = proofUrl.removePrefix("https://gist.github.com/").split("?")[0].split("/")
GitHubIdentity(path[0], path[1])
} catch (e: Exception) {
null
}
}
}
}
class TwitterIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://twitter.com/$identity/status/$proof"
override fun platform() = platform
companion object {
val platform = "twitter"
fun parseProofUrl(proofUrl: String): TwitterIdentity? {
return try {
if (proofUrl.isBlank()) return null
val path = proofUrl.removePrefix("https://twitter.com/").split("?")[0].split("/")
TwitterIdentity(path[0], path[2])
} catch (e: Exception) {
null
}
}
}
}
class TelegramIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://t.me/$proof"
override fun platform() = platform
companion object {
val platform = "telegram"
}
}
class MastodonIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://$identity/$proof"
override fun platform() = platform
companion object {
val platform = "mastodon"
fun parseProofUrl(proofUrl: String): MastodonIdentity? {
return try {
if (proofUrl.isBlank()) return null
val path = proofUrl.removePrefix("https://").split("?")[0].split("/")
return MastodonIdentity("${path[0]}/${path[1]}", path[2])
} catch (e: Exception) {
null
}
}
}
}
class MetadataEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun contactMetaData() = try {
mapper.readValue(
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
UserMetadata::class.java
)
} catch (e: Exception) {
// e.printStackTrace()
Log.w("MT", "Content Parse Error: ${e.localizedMessage} $content")
null
}
fun identityClaims() = tags.filter { it.firstOrNull() == "i" }.mapNotNull {
try {
IdentityClaim.create(it.get(1), it.get(2))
} catch (e: Exception) {
Log.e("MetadataEvent", "Can't parse identity [${it.joinToString { "," }}]", e)
null
}
}
companion object {
const val kind = 0
fun create(contactMetaData: String, identities: List<IdentityClaim>, pubKey: HexKey, privateKey: ByteArray?, createdAt: Long = TimeUtils.now()): MetadataEvent {
val tags = mutableListOf<List<String>>()
identities.forEach {
tags.add(listOf("i", it.platformIdentity(), it.proof))
}
val id = generateId(pubKey, createdAt, kind, tags, contactMetaData)
val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey)
return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, contactMetaData, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: MetadataEvent, signature: String): MetadataEvent {
return MetadataEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,110 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class MuteListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun plainContent(privKey: ByteArray): String? {
return try {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray())
return CryptoUtils.decryptNIP04(content, sharedSecret)
} catch (e: Exception) {
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
null
}
}
@Transient
private var privateTagsCache: List<List<String>>? = null
fun privateTags(privKey: ByteArray): List<List<String>>? {
if (privateTagsCache != null) {
return privateTagsCache
}
privateTagsCache = try {
plainContent(privKey)?.let { mapper.readValue<List<List<String>>>(it) }
} catch (e: Throwable) {
Log.w("BookmarkList", "Error parsing the JSON ${e.message}")
null
}
return privateTagsCache
}
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) }
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "e" }?.mapNotNull { it.getOrNull(1) }
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 10000
fun create(
events: List<String>? = null,
users: List<String>? = null,
addresses: List<ATag>? = null,
privEvents: List<String>? = null,
privUsers: List<String>? = null,
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): MuteListEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val privTags = mutableListOf<List<String>>()
privEvents?.forEach {
privTags.add(listOf("e", it))
}
privUsers?.forEach {
privTags.add(listOf("p", it))
}
privAddresses?.forEach {
privTags.add(listOf("a", it.toTag()))
}
val msg = mapper.writeValueAsString(privTags)
val content = CryptoUtils.encryptNIP04(
msg,
privateKey,
pubKey
)
val tags = mutableListOf<List<String>>()
events?.forEach {
tags.add(listOf("e", it))
}
users?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return MuteListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,89 +0,0 @@
package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
class NIP24Factory {
fun createMsgNIP24(
msg: String,
to: List<HexKey>,
from: ByteArray,
subject: String? = null,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null
): List<GiftWrapEvent> {
val senderPublicKey = CryptoUtils.pubkeyCreate(from).toHexKey()
val senderMessage = ChatMessageEvent.create(
msg = msg,
to = to,
privateKey = from,
subject = subject,
replyTos = replyTos,
mentions = mentions,
zapReceiver = zapReceiver,
markAsSensitive = markAsSensitive,
zapRaiserAmount = zapRaiserAmount,
geohash = geohash
)
return to.plus(senderPublicKey).map {
GiftWrapEvent.create(
event = SealedGossipEvent.create(
event = senderMessage,
encryptTo = it,
privateKey = from
),
recipientPubKey = it
)
}
}
fun createReactionWithinGroup(content: String, originalNote: EventInterface, to: List<HexKey>, from: KeyPair): List<GiftWrapEvent> {
val senderPublicKey = CryptoUtils.pubkeyCreate(from.privKey!!).toHexKey()
val senderReaction = ReactionEvent.create(
content,
originalNote,
from
)
return to.plus(senderPublicKey).map {
GiftWrapEvent.create(
event = SealedGossipEvent.create(
event = senderReaction,
encryptTo = it,
privateKey = from.privKey
),
recipientPubKey = it
)
}
}
fun createReactionWithinGroup(emojiUrl: EmojiUrl, originalNote: EventInterface, to: List<HexKey>, from: KeyPair): List<GiftWrapEvent> {
val senderPublicKey = CryptoUtils.pubkeyCreate(from.privKey!!).toHexKey()
val senderReaction = ReactionEvent.create(
emojiUrl,
originalNote,
from
)
return to.plus(senderPublicKey).map {
GiftWrapEvent.create(
event = SealedGossipEvent.create(
event = senderReaction,
encryptTo = it,
privateKey = from.privKey
),
recipientPubKey = it
)
}
}
}
@@ -1,37 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class NNSEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
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 ip6() = tags.firstOrNull { it.size > 1 && it[0] == "ip6" }?.get(1)
fun version() = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.get(1)
companion object {
const val kind = 30053
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): NNSEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return NNSEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,236 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
@Immutable
class PeopleListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
var decryptedContent: String? = null
var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null
fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> {
publicAndPrivateUserCache?.let {
return it
}
val privateUserList = privateKey?.let {
privateTagsOrEmpty(privKey = it).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
} ?: emptySet()
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
return publicAndPrivateUserCache ?: persistentSetOf()
}
fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet<HexKey> {
publicAndPrivateUserCache?.let {
return it
}
val privateUserList = privateTagsOrEmpty(decryptedContent).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
return publicAndPrivateUserCache ?: persistentSetOf()
}
fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean {
return if (isPrivate) {
privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
} else {
isTaggedUser(idHex)
}
}
fun isTaggedUser(idHex: String, isPrivate: Boolean, content: String): Boolean {
return if (isPrivate) {
privateTagsOrEmpty(content).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
} else {
isTaggedUser(idHex)
}
}
companion object {
const val kind = 30000
const val blockList = "mute"
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptTags(listOf(listOf("p", pubKeyHex)), privateKey),
tags = listOf(listOf("d", name)),
privateKey = privateKey,
createdAt = createdAt
)
} else {
create(
content = "",
tags = listOf(listOf("d", name), listOf("p", pubKeyHex)),
privateKey = privateKey,
createdAt = createdAt
)
}
}
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = listOf(listOf("d", name)),
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = "",
tags = listOf(listOf("d", name), listOf("p", pubKeyHex)),
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptTags(
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(
listPubKeyHex.map {
listOf("p", it)
}
),
privateKey = privateKey
),
tags = earlierVersion.tags,
privateKey = privateKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(
listPubKeyHex.map {
listOf("p", it)
}
),
privateKey = privateKey,
createdAt = createdAt
)
}
}
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = earlierVersion.tags,
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
return if (isPrivate) {
create(
content = encryptTags(
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf("p", pubKeyHex)),
privateKey = privateKey
),
tags = earlierVersion.tags,
privateKey = privateKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
privateKey = privateKey,
createdAt = createdAt
)
}
}
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, encryptedContent: String, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (!earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
return if (isPrivate) {
create(
content = encryptTags(
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && it[1] != pubKeyHex },
privateKey = privateKey
),
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
privateKey = privateKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
privateKey = privateKey,
createdAt = createdAt
)
}
}
fun create(content: String, tags: List<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun create(content: String, tags: List<List<String>>, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent {
val id = generateId(pubKey, createdAt, kind, tags, content)
return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
}
fun create(unsignedEvent: PeopleListEvent, signature: String): PeopleListEvent {
return PeopleListEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,41 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class PinListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun pins() = tags.filter { it.size > 1 && it[0] == "pin" }.map { it[1] }
companion object {
const val kind = 33888
fun create(
pins: List<String>,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): PinListEvent {
val tags = mutableListOf<List<String>>()
pins.forEach {
tags.add(listOf("pin", it))
}
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return PinListEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,126 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
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: List<List<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] }
fun getTagInt(property: String): Int? {
val number = tags.firstOrNull() { it.size > 1 && it[0] == property }?.get(1)
return if (number.isNullOrBlank() || number == "null") {
null
} else {
number.toInt()
}
}
companion object {
const val kind = 6969
fun create(
msg: String,
replyTos: List<String>?,
mentions: List<String>?,
addresses: List<ATag>?,
pubKey: HexKey,
privateKey: ByteArray?,
createdAt: Long = TimeUtils.now(),
pollOptions: Map<Int, String>,
valueMaximum: Int?,
valueMinimum: Int?,
consensusThreshold: Int?,
closedAt: Int?,
zapReceiver: String?,
markAsSensitive: Boolean,
zapRaiserAmount: Long?,
geohash: String? = null
): PollNoteEvent {
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
}
pollOptions.forEach { poll_op ->
tags.add(listOf(POLL_OPTION, poll_op.key.toString(), poll_op.value))
}
tags.add(listOf(VALUE_MAXIMUM, valueMaximum.toString()))
tags.add(listOf(VALUE_MINIMUM, valueMinimum.toString()))
tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString()))
tags.add(listOf(CLOSED_AT, closedAt.toString()))
if (zapReceiver != null) {
tags.add(listOf("zap", zapReceiver))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey)
return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: PollNoteEvent, signature: String
): PollNoteEvent {
return PollNoteEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
/*
{
"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>
}
*/
@@ -1,140 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Hex
import kotlinx.collections.immutable.persistentSetOf
@Immutable
class PrivateDmEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable {
/**
* This may or may not be the actual recipient's pub key. The event is intended to look like a
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
* for initial messages.
*/
private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull()
fun verifiedRecipientPubKey(): HexKey? {
val recipient = recipientPubKey()
return if (HexValidator.isHex(recipient)) {
recipient
} else {
null
}
}
fun talkingWith(oneSideHex: String): HexKey {
return if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey
}
override fun chatroomKey(toRemove: String): ChatroomKey {
return ChatroomKey(persistentSetOf(talkingWith(toRemove)))
}
/**
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
*
* Nip-18 messages should refer to other events by inline references in the content like
* `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506).
*/
fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun with(pubkeyHex: String): Boolean {
return pubkeyHex == pubKey ||
tags.any { it.size > 1 && it[0] == "p" && it[1] == pubkeyHex }
}
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
return try {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey)
val retVal = CryptoUtils.decryptNIP04(content, sharedSecret)
if (retVal.startsWith(nip18Advertisement)) {
retVal.substring(16)
} else {
retVal
}
} catch (e: Exception) {
Log.w("PrivateDM", "Error decrypting the message ${e.message}")
null
}
}
companion object {
const val kind = 4
const val nip18Advertisement = "[//]: # (nip18)\n"
fun create(
recipientPubKey: ByteArray,
msg: String,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String?,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now(),
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true,
markAsSensitive: Boolean,
zapRaiserAmount: Long?,
geohash: String? = null
): PrivateDmEvent {
val message = if (advertiseNip18) { nip18Advertisement } else { "" } + msg
val content = if (keyPair.privKey == null) message else CryptoUtils.encryptNIP04(
message,
keyPair.privKey,
recipientPubKey
)
val tags = mutableListOf<List<String>>()
publishedRecipientPubKey?.let {
tags.add(listOf("p", publishedRecipientPubKey.toHexKey()))
}
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: PrivateDmEvent,
signature: String,
): PrivateDmEvent {
return PrivateDmEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,69 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class ReactionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun originalPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
fun originalAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
companion object {
const val kind = 7
fun createWarning(originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent {
return create("\u26A0\uFE0F", originalNote, keyPair, createdAt)
}
fun createLike(originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent {
return create("+", originalNote, keyPair, createdAt)
}
fun create(content: String, originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent {
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
if (originalNote is AddressableEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(emojiUrl: EmojiUrl, originalNote: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReactionEvent {
val content = ":${emojiUrl.code}:"
val pubKey = keyPair.pubKey.toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("emoji", emojiUrl.code, emojiUrl.url)
)
if (originalNote is AddressableEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: ReactionEvent, signature: String): ReactionEvent {
return ReactionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,34 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
import java.net.URI
@Immutable
class RecommendRelayEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun relay() = URI.create(content.trim())
companion object {
const val kind = 2
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RecommendRelayEvent {
val content = relay.toString()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,40 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class RelayAuthEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun relay() = tags.firstOrNull() { it.size > 1 && it[0] == "relay" }?.get(1)
fun challenge() = tags.firstOrNull() { it.size > 1 && it[0] == "challenge" }?.get(1)
companion object {
const val kind = 22242
fun create(relay: String, challenge: String, pubKey: HexKey, privateKey: ByteArray?, createdAt: Long = TimeUtils.now()): RelayAuthEvent {
val content = ""
val tags = listOf(
listOf("relay", relay),
listOf("challenge", challenge)
)
val localPubKey = if (pubKey.isBlank() && privateKey != null) CryptoUtils.pubkeyCreate(privateKey).toHexKey() else pubKey
val id = generateId(localPubKey, createdAt, kind, tags, content)
val sig = if (privateKey == null) null else CryptoUtils.sign(id, privateKey)
return RelayAuthEvent(id.toHexKey(), localPubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: RelayAuthEvent, signature: String): RelayAuthEvent {
return RelayAuthEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,41 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class RelaySetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun relays() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
fun description() = tags.firstOrNull() { it.size > 1 && it[0] == "description" }?.get(1)
companion object {
const val kind = 30022
fun create(
relays: List<String>,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): RelaySetEvent {
val tags = mutableListOf<List<String>>()
relays.forEach {
tags.add(listOf("r", it))
}
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return RelaySetEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -1,100 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
// NIP 56 event.
@Immutable
class ReportEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
private fun defaultReportType(): ReportType {
// Works with old and new structures for report.
var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.uppercase()) }.firstOrNull()
if (reportType == null) {
reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.uppercase()) }.firstOrNull()
}
if (reportType == null) {
reportType = ReportType.SPAM
}
return reportType
}
fun reportedPost() = tags
.filter { it.size > 1 && it[0] == "e" }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.uppercase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType()
)
}
fun reportedAuthor() = tags
.filter { it.size > 1 && it[0] == "p" }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.uppercase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType()
)
}
companion object {
const val kind = 1984
fun create(
reportedPost: EventInterface,
type: ReportType,
keyPair: KeyPair,
content: String = "",
createdAt: Long = TimeUtils.now()
): ReportEvent {
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
val pubKey = keyPair.pubKey.toHexKey()
var tags: List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is AddressableEvent) {
tags = tags + listOf(listOf("a", reportedPost.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(reportedUser: String, type: ReportType, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ReportEvent {
val content = ""
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
val pubKey = keyPair.pubKey.toHexKey()
val tags: List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
}
enum class ReportType() {
EXPLICIT, // Not used anymore.
ILLEGAL,
SPAM,
IMPERSONATION,
NUDITY,
PROFANITY
}
}
@@ -1,54 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class RepostEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun boostedPost() = taggedEvents()
fun originalAuthor() = taggedUsers()
fun containedPost() = try {
fromJson(content)
} catch (e: Exception) {
null
}
companion object {
const val kind = 6
fun create(boostedPost: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): RepostEvent {
val content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id())
val replyToAuthor = listOf("p", boostedPost.pubKey())
val pubKey = keyPair.pubKey.toHexKey()
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
if (boostedPost is AddressableEvent) {
tags = tags + listOf(listOf("a", boostedPost.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
}
fun create(unsignedEvent: RepostEvent, signature: String): RepostEvent {
return RepostEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,157 +0,0 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.Nip44Version
import com.vitorpamplona.quartz.crypto.decodeNIP44
import com.vitorpamplona.quartz.crypto.encodeNIP44
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class SealedGossipEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
): WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient
private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
fun cachedGossip(privKey: ByteArray): Event? {
val hex = privKey.toHexKey()
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val gossip = unseal(privKey = privKey)
val event = gossip?.mergeWith(this)
if (event is WrappedEvent) {
event.host = host ?: this
}
cachedInnerEvent = cachedInnerEvent + Pair(hex, event)
return event
}
fun cachedGossip(pubKey: ByteArray, decryptedContent: String): Event? {
val hex = pubKey.toHexKey()
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
val gossip = unseal(decryptedContent)
val event = gossip?.mergeWith(this)
if (event is WrappedEvent) {
event.host = host ?: this
}
cachedInnerEvent = cachedInnerEvent + Pair(hex, event)
return event
}
fun unseal(privKey: ByteArray): Gossip? = try {
plainContent(privKey)?.let { Gossip.fromJson(it) }
} catch (e: Exception) {
Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e)
null
}
fun unseal(decryptedContent: String): Gossip? = try {
plainContent(decryptedContent)?.let { Gossip.fromJson(it) }
} catch (e: Exception) {
Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e)
null
}
private fun plainContent(decryptedContent: String): String? {
if (decryptedContent.isEmpty()) return null
return decryptedContent
}
private fun plainContent(privKey: ByteArray): String? {
if (content.isEmpty()) return null
return try {
val toDecrypt = decodeNIP44(content) ?: return null
return when (toDecrypt.v) {
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray())
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray())
else -> null
}
} catch (e: Exception) {
Log.w("GossipEvent", "Error decrypting the message ${e.message}")
null
}
}
companion object {
const val kind = 13
fun create(
event: Event,
encryptTo: HexKey,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): SealedGossipEvent {
val gossip = Gossip.create(event)
return create(gossip, encryptTo, privateKey, createdAt)
}
fun create(
gossip: Gossip,
encryptTo: HexKey,
privateKey: ByteArray,
createdAt: Long = TimeUtils.randomWithinAWeek()
): SealedGossipEvent {
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, encryptTo.hexToByteArray())
val content = encodeNIP44(
CryptoUtils.encryptNIP44(
Gossip.toJson(gossip),
sharedSecret
)
)
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return SealedGossipEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
class Gossip(
val id: HexKey?,
@JsonProperty("pubkey")
val pubKey: HexKey?,
@JsonProperty("created_at")
val createdAt: Long?,
val kind: Int?,
val tags: List<List<String>>?,
val content: String?
) {
fun mergeWith(event: SealedGossipEvent): Event {
val newPubKey = pubKey?.ifBlank { null } ?: event.pubKey
val newCreatedAt = if (createdAt != null && createdAt > 1000) createdAt else event.createdAt
val newKind = kind ?: -1
val newTags = (tags ?: emptyList()).plus(event.tags)
val newContent = content ?: ""
val newID = id?.ifBlank { null } ?: Event.generateId(newPubKey, newCreatedAt, newKind, newTags, newContent).toHexKey()
val sig = ""
return EventFactory.create(newID, newPubKey, newCreatedAt, newKind, newTags, newContent, sig)
}
companion object {
fun fromJson(json: String): Gossip = Event.mapper.readValue(json, Gossip::class.java)
fun toJson(event: Gossip): String = Event.mapper.writeValueAsString(event)
fun create(event: Event): Gossip {
return Gossip(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content)
}
}
}
@@ -1,75 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class StatusEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 30315
fun create(
msg: String,
type: String,
expiration: Long?,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): StatusEvent {
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", type))
expiration?.let { tags.add(listOf("expiration", it.toString())) }
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "")
}
fun update(
event: StatusEvent,
newStatus: String,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): StatusEvent {
val tags = event.tags
val pubKey = event.pubKey()
val id = generateId(pubKey, createdAt, kind, tags, newStatus)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig?.toHexKey() ?: "")
}
fun clear(
event: StatusEvent,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): StatusEvent {
val msg = ""
val tags = event.tags.filter { it.size > 1 && it[0] == "d" }
val pubKey = event.pubKey()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: StatusEvent,
signature: String
): StatusEvent {
return StatusEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
@@ -1,115 +0,0 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class TextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun root() = tags.firstOrNull() { it.size > 3 && it[3] == "root" }?.get(1)
companion object {
const val kind = 1
fun create(
msg: String,
replyTos: List<String>?,
mentions: List<String>?,
addresses: List<ATag>?,
extraTags: List<String>?,
zapReceiver: String?,
markAsSensitive: Boolean,
zapRaiserAmount: Long?,
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
geohash: String? = null,
keyPair: KeyPair,
createdAt: Long = TimeUtils.now()
): TextNoteEvent {
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
if (it == replyingTo) {
tags.add(listOf("e", it, "", "reply"))
} else if (it == root) {
tags.add(listOf("e", it, "", "root"))
} else if (it in directMentions) {
tags.add(listOf("e", it, "", "mention"))
} else {
tags.add(listOf("e", it))
}
}
mentions?.forEach {
if (it in directMentions) {
tags.add(listOf("p", it, "", "mention"))
} else {
tags.add(listOf("p", it))
}
}
addresses?.forEach {
val aTag = it.toTag()
if (aTag == replyingTo) {
tags.add(listOf("a", aTag, "", "reply"))
} else if (aTag == root) {
tags.add(listOf("a", aTag, "", "root"))
} else if (aTag in directMentions) {
tags.add(listOf("a", aTag, "", "mention"))
} else {
tags.add(listOf("a", aTag))
}
}
findHashtags(msg).forEach {
tags.add(listOf("t", it))
tags.add(listOf("t", it.lowercase()))
}
extraTags?.forEach {
tags.add(listOf("t", it))
}
zapReceiver?.let {
tags.add(listOf("zap", it))
}
findURLs(msg).forEach {
tags.add(listOf("r", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val pubKey = keyPair.pubKey.toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "")
}
fun create(
unsignedEvent: TextNoteEvent, signature: String
): TextNoteEvent {
return TextNoteEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
}
}
}
fun findURLs(text: String): List<String> {
return UrlDetector(text, UrlDetectorOptions.Default).detect().map { it.originalUrl }
}
@@ -1,21 +0,0 @@
package com.vitorpamplona.quartz.utils
import com.vitorpamplona.quartz.crypto.CryptoUtils
object TimeUtils {
const val oneMinute = 60
const val fiveMinutes = 5 * oneMinute
const val oneHour = 60 * oneMinute
const val eightHours = 8 * oneHour
const val oneDay = 24 * oneHour
const val oneWeek = 7 * oneDay
fun now() = System.currentTimeMillis() / 1000
fun fiveMinutesAgo() = now() - fiveMinutes
fun oneHourAgo() = now() - oneHour
fun oneHourAhead() = now() + oneHour
fun oneDayAgo() = now() - oneDay
fun eightHoursAgo() = now() - eightHours
fun oneWeekAgo() = now() - oneWeek
fun randomWithinAWeek() = System.currentTimeMillis() / 1000 - CryptoUtils.randomInt(oneWeek)
}
@@ -1,173 +0,0 @@
package com.vitorpamplona.quartz.encoders
import org.junit.Assert.assertEquals
import org.junit.Test
class NIP19ParserTest {
@Test
fun nAddrParser() {
val result = Nip19.uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus")
assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex)
}
@Test
fun nAddrParser2() {
val result = Nip19.uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8")
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
}
@Test
fun nAddrParse3() {
val result = Nip19.uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay)
}
@Test
fun nAddrATagParse3() {
val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io")
assertEquals(30023, address?.kind)
assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex)
assertEquals("89de7920", address?.dTag)
assertEquals("wss://relay.damus.io", address?.relay)
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr())
}
@Test
fun nAddrFormatter() {
val address = ATag(
30023,
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
"",
null
)
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
}
@Test
fun nAddrFormatter2() {
val address = ATag(
30023,
"d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c",
"guide-wireguard",
null
)
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
}
@Test
fun nAddrFormatter3() {
val address = ATag(
30023,
"d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193",
"89de7920",
"wss://relay.damus.io"
)
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
}
@Test
fun nAddrParserPablo() {
val result = Nip19.uriToRoute("naddr1qq2hs7p30p6kcunxxamkgcnyd33xxve3veshyq3qyujphdcz69z6jafxpnldae3xtymdekfeatkt3r4qusr3w5krqspqxpqqqpaxjlg805f")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far", result?.hex)
assertEquals(null, result?.relay)
assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result?.author)
assertEquals(31337, result?.kind)
}
@Test
fun nAddrParserGizmo() {
val result = Nip19.uriToRoute("naddr1qpqrvvfnvccrzdryxgunzvtxvgukge34xfjnqdpcv9sk2desxgmrscesvserzd3h8ycrywphvg6nsvf58ycnqef3v5mnsvt98pjnqdfs8ypzq3huhccxt6h34eupz3jeynjgjgek8lel2f4adaea0svyk94a3njdqvzqqqr4gudhrkyk")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result?.hex)
assertEquals(null, result?.relay)
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
assertEquals(30023, result?.kind)
}
@Test
fun nAddrParserGizmo2() {
val result = Nip19.uriToRoute("naddr1qq9rzd3h8y6nqwf5xyuqygzxljlrqe027xh8sy2xtyjwfzfrxcll8afxh4hh847psjckhkxwf5psgqqqw4rsty50fx")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
assertEquals(null, result?.relay)
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
assertEquals(30023, result?.kind)
}
@Test
fun nEventParserTest() {
val result = Nip19.uriToRoute("nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy")
assertEquals(Nip19.Type.EVENT, result?.type)
assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex)
assertEquals(null, result?.relay)
assertEquals(null, result?.author)
assertEquals(null, result?.kind)
}
@Test
fun nEventParser() {
val result = Nip19.uriToRoute("nostr:nevent1qqstvrl6wftd8ht4g0vrp6m30tjs6pdxcvk977g769dcvlptkzu4ftqppamhxue69uhkummnw3ezumt0d5pzp78lz8r60568sd2a8dx3wnj6gume02gxaf92vx4fk67qv5kpagt6qvzqqqqqqygqr86c")
assertEquals(Nip19.Type.EVENT, result?.type)
assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex)
assertEquals("wss://nostr.mom", result?.relay)
assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventParser2() {
val result = Nip19.uriToRoute("nostr:nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w")
assertEquals(Nip19.Type.EVENT, result?.type)
assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay)
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventParser3() {
val result = Nip19.uriToRoute("nostr:nevent1qqsg6gechd3dhzx38n4z8a2lylzgsmmgeamhmtzz72m9ummsnf0xjfspsdmhxue69uhkummn9ekx7mpvwaehxw309ahx7um5wghx77r5wghxgetk93mhxue69uhhyetvv9ujumn0wd68ytnzvuk8wumn8ghj7mn0wd68ytn9d9h82mny0fmkzmn6d9njuumsv93k2trhwden5te0wfjkccte9ehx7um5wghxyctwvsk8wumn8ghj7un9d3shjtnyv9kh2uewd9hs3kqsdn")
assertEquals(Nip19.Type.EVENT, result?.type)
assertEquals("8d2338bb62db88d13cea23f55f27c4886f68cf777dac42f2b65e6f709a5e6926", result?.hex)
assertEquals("wss://nos.lol,wss://nostr.oxtr.dev,wss://relay.nostr.bg,wss://nostr.einundzwanzig.space,wss://relay.nostr.band,wss://relay.damus.io", result?.relay)
}
@Test
fun nEventParserInvalidChecksum() {
val result = Nip19.uriToRoute("nostr:nevent1qqsyxq8v0730nz38dupnjzp5jegkyz4gu2ptwcps4v32hjnrap0q0espz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqyn3t9gj")
assertEquals(Nip19.Type.EVENT, result?.type)
assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay)
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventFormatter() {
val nevent = Nip19.createNEvent("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", null, null, null)
assertEquals("nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy", nevent)
}
@Test
fun nEventFormatterWithExtraInfo() {
val nevent = Nip19.createNEvent("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 40, null)
assertEquals("nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhqzypl62m6ad932k83u6sjwwkxrqq4cve0hkrvdem5la83g34m4rtqegqcyqqqqq2qh26va4", nevent)
}
@Test
fun nEventFormatterWithFullInfo() {
val nevent = Nip19.createNEvent(
"1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
1,
"wss://relay.damus.io"
)
assertEquals("nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w", nevent)
}
}
@@ -1,81 +0,0 @@
package com.vitorpamplona.quartz.encoders
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
class Nip19Test {
@Test()
fun uri_to_route_null() {
val actual = Nip19.uriToRoute(null)
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_unknown() {
val actual = Nip19.uriToRoute("nostr:unknown")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_npub() {
val actual =
Nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y")
Assert.assertEquals(Nip19.Type.USER, actual?.type)
Assert.assertEquals(
"bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365",
actual?.hex
)
}
@Test()
fun uri_to_route_note() {
val actual =
Nip19.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")
Assert.assertEquals(Nip19.Type.NOTE, actual?.type)
Assert.assertEquals(
"82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3",
actual?.hex
)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_nprofile() {
val actual = Nip19.uriToRoute("nostr:nprofile")
Assert.assertEquals(Nip19.Type.USER, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_nevent() {
val actual = Nip19.uriToRoute("nostr:nevent")
Assert.assertEquals(Nip19.Type.USER, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_nrelay() {
val actual = Nip19.uriToRoute("nostr:nrelay")
Assert.assertEquals(Nip19.Type.RELAY, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_naddr() {
val actual = Nip19.uriToRoute("nostr:naddr")
Assert.assertEquals(Nip19.Type.ADDRESS, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
}
@@ -1,38 +0,0 @@
package com.vitorpamplona.quartz.encoders
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
class TlvIntegerTest {
fun to_int_32_length_smaller_than_4() {
Assert.assertNull(byteArrayOfInts(1, 2, 3).toInt32())
}
fun to_int_32_length_bigger_than_4() {
Assert.assertNull(byteArrayOfInts(1, 2, 3, 4, 5).toInt32())
}
@Test()
fun to_int_32_length_4() {
val actual = byteArrayOfInts(1, 2, 3, 4).toInt32()
assertEquals(16909060, actual)
}
@Test()
fun backAndForth() {
assertEquals(234, 234.to32BitByteArray().toInt32())
assertEquals(1, 1.to32BitByteArray().toInt32())
assertEquals(0, 0.to32BitByteArray().toInt32())
assertEquals(1000, 1000.to32BitByteArray().toInt32())
assertEquals(-234, (-234).to32BitByteArray().toInt32())
assertEquals(-1, (-1).to32BitByteArray().toInt32())
assertEquals(-0, (-0).to32BitByteArray().toInt32())
assertEquals(-1000, (-1000).to32BitByteArray().toInt32())
}
private fun byteArrayOfInts(vararg ints: Int) =
ByteArray(ints.size) { pos -> ints[pos].toByte() }
}
-1
View File
@@ -16,4 +16,3 @@ dependencyResolutionManagement {
} }
rootProject.name = "Nostr Signer" rootProject.name = "Nostr Signer"
include ':app' include ':app'
include ':quartz'