mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
nip44
This commit is contained in:
@@ -68,6 +68,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':quartz')
|
||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
@@ -120,4 +121,6 @@ dependencies {
|
||||
|
||||
// Biometrics
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
|
||||
|
||||
}
|
||||
Vendored
+51
-12
@@ -5,17 +5,56 @@
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# 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
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
-keepdirectories libs
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# 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.** { *; }
|
||||
|
||||
@@ -4,7 +4,8 @@ class IntentData(
|
||||
val data: String,
|
||||
val name: String,
|
||||
val type: SignerType,
|
||||
val pubKey: HexKey
|
||||
val pubKey: HexKey,
|
||||
val id: String
|
||||
)
|
||||
|
||||
enum class SignerType {
|
||||
|
||||
@@ -9,19 +9,27 @@ import java.net.URLDecoder
|
||||
object IntentUtils {
|
||||
fun getIntentData(intent: Intent): IntentData? {
|
||||
if (intent.data != null) {
|
||||
val data = try {
|
||||
URLDecoder.decode(intent.data?.toString()?.replace("+", "%2b") ?: "", "utf-8").replace("nostrsigner:", "")
|
||||
} catch (e: Exception) {
|
||||
intent.data?.toString()?.replace("nostrsigner:", "") ?: ""
|
||||
}
|
||||
|
||||
val type = when (intent.extras?.getString("type")) {
|
||||
"sign_event" -> SignerType.SIGN_EVENT
|
||||
"nip04_encrypt" -> SignerType.NIP04_ENCRYPT
|
||||
"nip04_decrypt" -> SignerType.NIP04_DECRYPT
|
||||
"nip44_decrypt" -> SignerType.NIP44_DECRYPT
|
||||
"nip44_encrypt" -> SignerType.NIP44_ENCRYPT
|
||||
"get_public_key" -> SignerType.GET_PUBLIC_KEY
|
||||
else -> SignerType.SIGN_EVENT
|
||||
}
|
||||
|
||||
val data = try {
|
||||
when (type) {
|
||||
SignerType.NIP44_ENCRYPT, SignerType.NIP04_DECRYPT, SignerType.NIP44_DECRYPT, SignerType.NIP04_ENCRYPT -> {
|
||||
intent.data?.toString()?.replace("nostrsigner:", "") ?: ""
|
||||
}
|
||||
else -> URLDecoder.decode(intent.data?.toString()?.replace("+", "%2b") ?: "", "utf-8").replace("nostrsigner:", "")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
intent.data?.toString()?.replace("nostrsigner:", "") ?: ""
|
||||
}
|
||||
|
||||
val split = data.split(";")
|
||||
var name = ""
|
||||
if (split.isNotEmpty()) {
|
||||
@@ -30,8 +38,9 @@ object IntentUtils {
|
||||
}
|
||||
}
|
||||
val pubKey = intent.extras?.getString("pubKey") ?: ""
|
||||
val id = intent.extras?.getString("id") ?: ""
|
||||
|
||||
return IntentData(data, name, type, pubKey)
|
||||
return IntentData(data, name, type, pubKey, id)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.models.hexToByteArray
|
||||
import com.greenart7c3.nostrsigner.models.toHexKey
|
||||
import com.greenart7c3.nostrsigner.service.CryptoUtils
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
@@ -70,9 +69,17 @@ import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.components.Drawer
|
||||
import com.greenart7c3.nostrsigner.ui.components.MainAppBar
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils.decryptNIP04
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils.decryptNIP44
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils.encryptNIP44
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils.getSharedSecretNIP44
|
||||
import com.vitorpamplona.quartz.crypto.Nip44Version
|
||||
import com.vitorpamplona.quartz.crypto.decodeNIP44
|
||||
import com.vitorpamplona.quartz.crypto.encodeNIP44
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray as hexToByteArray1
|
||||
|
||||
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
|
||||
@Composable
|
||||
@@ -149,8 +156,8 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
|
||||
}
|
||||
)
|
||||
}
|
||||
SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT -> {
|
||||
val key = if (it.type == SignerType.NIP04_DECRYPT) {
|
||||
SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT -> {
|
||||
val key = if (it.type == SignerType.NIP04_DECRYPT || it.type == SignerType.NIP44_DECRYPT) {
|
||||
"$packageName-decryptnip4"
|
||||
} else {
|
||||
"$packageName-encryptnip4"
|
||||
@@ -167,19 +174,45 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
|
||||
it.type,
|
||||
{
|
||||
try {
|
||||
val sig = if (it.type == SignerType.NIP04_DECRYPT) {
|
||||
CryptoUtils.decrypt(
|
||||
it.data,
|
||||
account.keyPair.privKey,
|
||||
Hex.decode(it.pubKey)
|
||||
)
|
||||
} else {
|
||||
CryptoUtils.encrypt(
|
||||
it.data,
|
||||
account.keyPair.privKey,
|
||||
Hex.decode(it.pubKey)
|
||||
)
|
||||
val sig = try {
|
||||
when (it.type) {
|
||||
SignerType.NIP04_DECRYPT -> {
|
||||
CryptoUtils.decrypt(
|
||||
it.data,
|
||||
account.keyPair.privKey,
|
||||
Hex.decode(it.pubKey)
|
||||
)
|
||||
}
|
||||
SignerType.NIP04_ENCRYPT -> {
|
||||
CryptoUtils.encrypt(
|
||||
it.data,
|
||||
account.keyPair.privKey,
|
||||
Hex.decode(it.pubKey)
|
||||
)
|
||||
}
|
||||
SignerType.NIP44_ENCRYPT -> {
|
||||
val sharedSecret = getSharedSecretNIP44(account.keyPair.privKey, it.pubKey.hexToByteArray1())
|
||||
|
||||
encodeNIP44(
|
||||
encryptNIP44(
|
||||
it.data,
|
||||
sharedSecret
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val toDecrypt = decodeNIP44(it.data) ?: return@EncryptDecryptData
|
||||
when (toDecrypt.v) {
|
||||
Nip44Version.NIP04.versionCode -> decryptNIP04(toDecrypt, account.keyPair.privKey, it.pubKey.hexToByteArray1())
|
||||
Nip44Version.NIP44.versionCode -> decryptNIP44(toDecrypt, account.keyPair.privKey, it.pubKey.hexToByteArray1())
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
} ?: "Could not decrypt the message"
|
||||
} catch (e: Exception) {
|
||||
"Could not decrypt the message"
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
val activity = context.getAppCompatActivity()
|
||||
if (packageName != null) {
|
||||
@@ -187,6 +220,9 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
val intent = Intent()
|
||||
intent.putExtra("signature", sig)
|
||||
if (it.type == SignerType.NIP44_DECRYPT || it.type == SignerType.NIP04_DECRYPT) {
|
||||
intent.putExtra("id", it.id)
|
||||
}
|
||||
activity?.setResult(RESULT_OK, intent)
|
||||
} else {
|
||||
clipboardManager.setText(AnnotatedString(sig))
|
||||
@@ -242,7 +278,7 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
|
||||
return@EventData
|
||||
}
|
||||
|
||||
val id = event.id.hexToByteArray()
|
||||
val id = event.id.hexToByteArray1()
|
||||
val sig = CryptoUtils.sign(id, account.keyPair.privKey).toHexKey()
|
||||
|
||||
coroutineScope.launch {
|
||||
|
||||
@@ -5,6 +5,7 @@ buildscript {
|
||||
lifecycle_version = '2.6.1'
|
||||
compose_ui_version = '1.5.0'
|
||||
nav_version = "2.7.0"
|
||||
core_ktx_version = '1.10.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ plugins {
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
id 'com.android.library' version '8.1.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.0' apply false
|
||||
}
|
||||
|
||||
tasks.register('installGitHook', Copy) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,58 @@
|
||||
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'
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
# 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
@@ -0,0 +1,20 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,304 @@
|
||||
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() }
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,34 @@
|
||||
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()}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
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)
|
||||
@@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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?)
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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?)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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?
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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>
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
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?)
|
||||
@@ -0,0 +1,39 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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)
|
||||
@@ -0,0 +1,75 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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?
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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>
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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 }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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() }
|
||||
}
|
||||
@@ -16,3 +16,4 @@ dependencyResolutionManagement {
|
||||
}
|
||||
rootProject.name = "Nostr Signer"
|
||||
include ':app'
|
||||
include ':quartz'
|
||||
|
||||
Reference in New Issue
Block a user