Merge upstream with local changes.
This commit is contained in:
+2
-2
@@ -59,7 +59,7 @@ dependencies {
|
||||
// LibSodium for ChaCha encryption (NIP-44)
|
||||
// Wait for @aar support in version catalogs
|
||||
implementation "com.goterl:lazysodium-android:5.1.0@aar"
|
||||
implementation 'net.java.dev.jna:jna:5.14.0@aar'
|
||||
implementation 'net.java.dev.jna:jna:5.15.0@aar'
|
||||
|
||||
//implementation (libs.lazysodium.android) { artifact { type = "aar" } }
|
||||
//implementation (libs.jna) { artifact { type = "aar" } }
|
||||
@@ -73,7 +73,7 @@ dependencies {
|
||||
// Parses URLs from Text:
|
||||
api libs.url.detector
|
||||
|
||||
// Parses URLs from Text:
|
||||
// Normalizes URLs
|
||||
api libs.rfc3986.normalizer
|
||||
|
||||
testImplementation libs.junit
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.crypto.nip17
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils.decrypt
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AESGCMTest {
|
||||
val decryptionNonce = "01e77c94bd5aba3e3cbb69594e7ba07c"
|
||||
val decryptionKey = "c128ecffab90ee7810e3df08e7fb2cc39a8d40f24201f48b2b36e23b34ac50ee"
|
||||
|
||||
val cipher = AESGCM(decryptionKey.hexToByteArray(), decryptionNonce.toByteArray(Charsets.UTF_8))
|
||||
|
||||
@Test
|
||||
fun encryptDecrypt() {
|
||||
val encrypted = cipher.encrypt("Testing".toByteArray(Charsets.UTF_8))
|
||||
val decrypted = cipher.decrypt(encrypted)
|
||||
|
||||
assertEquals("Testing", String(decrypted))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun imageTest() {
|
||||
val image =
|
||||
getInstrumentation().context.assets.open("ovxxk2vz.jpg").use {
|
||||
it.readAllBytes()
|
||||
}
|
||||
|
||||
val decrypted = cipher.decrypt(image)
|
||||
|
||||
assertEquals(44201, decrypted.size)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.encoders
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -69,4 +71,43 @@ class HexEncodingTest {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsHex() {
|
||||
assertFalse("/0", HexValidator.isHex("/0"))
|
||||
assertFalse("/.", HexValidator.isHex("/."))
|
||||
assertFalse("!!", HexValidator.isHex("!!"))
|
||||
assertFalse("::", HexValidator.isHex("::"))
|
||||
assertFalse("@@", HexValidator.isHex("@@"))
|
||||
assertFalse("GG", HexValidator.isHex("GG"))
|
||||
assertFalse("FG", HexValidator.isHex("FG"))
|
||||
assertFalse("`a", HexValidator.isHex("`a"))
|
||||
assertFalse("gg", HexValidator.isHex("gg"))
|
||||
assertFalse("fg", HexValidator.isHex("fg"))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Test
|
||||
fun testRandomsIsHex() {
|
||||
for (i in 0..10000) {
|
||||
val bytes = CryptoUtils.privkeyCreate()
|
||||
val hex = bytes.toHexString(HexFormat.Default)
|
||||
assertTrue(hex, HexValidator.isHex(hex))
|
||||
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
|
||||
assertTrue(hexUpper, HexValidator.isHex(hexUpper))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Test
|
||||
fun testRandomsUppercase() {
|
||||
for (i in 0..1000) {
|
||||
val bytes = CryptoUtils.privkeyCreate()
|
||||
val hex = bytes.toHexString(HexFormat.UpperCase)
|
||||
assertEquals(
|
||||
bytes.toList(),
|
||||
Hex.decode(hex).toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.crypto.nip17
|
||||
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
interface NostrCipher {
|
||||
fun name(): String
|
||||
|
||||
fun encrypt(bytesToEncrypt: ByteArray): ByteArray
|
||||
|
||||
fun decrypt(bytesToDecrypt: ByteArray): ByteArray
|
||||
}
|
||||
|
||||
class AESGCM(
|
||||
val keyBytes: ByteArray = CryptoUtils.random(32),
|
||||
val nonce: ByteArray = CryptoUtils.random(16),
|
||||
) : NostrCipher {
|
||||
private fun newCipher() = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
|
||||
private fun keySpec() = SecretKeySpec(keyBytes, "AES")
|
||||
|
||||
private fun param() = GCMParameterSpec(128, nonce)
|
||||
|
||||
override fun name() = NAME
|
||||
|
||||
fun copyUsingUTF8Nonce(): AESGCM =
|
||||
AESGCM(
|
||||
keyBytes,
|
||||
nonce.toHexKey().toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
|
||||
override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
|
||||
with(newCipher()) {
|
||||
init(Cipher.ENCRYPT_MODE, keySpec(), param())
|
||||
doFinal(bytesToEncrypt)
|
||||
}
|
||||
|
||||
override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
|
||||
with(newCipher()) {
|
||||
init(Cipher.DECRYPT_MODE, keySpec(), param())
|
||||
doFinal(bytesToDecrypt)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAME = "aes-gcm"
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,25 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||
|
||||
@Immutable
|
||||
data class ATag(
|
||||
val kind: Int,
|
||||
val pubKeyHex: String,
|
||||
val dTag: String,
|
||||
val relay: String?,
|
||||
) {
|
||||
var relay: String? = null
|
||||
|
||||
constructor(
|
||||
kind: Int,
|
||||
pubKeyHex: String,
|
||||
dTag: String,
|
||||
relayHint: String?,
|
||||
) : this(kind, pubKeyHex, dTag) {
|
||||
this.relay = relayHint
|
||||
}
|
||||
|
||||
fun countMemory(): Long =
|
||||
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
|
||||
8L + // kind
|
||||
@@ -41,11 +52,15 @@ data class ATag(
|
||||
|
||||
fun toTag() = assembleATag(kind, pubKeyHex, dTag)
|
||||
|
||||
fun toNAddr(): String =
|
||||
fun toATagArray() = removeTrailingNullsAndEmptyOthers("a", toTag(), relay)
|
||||
|
||||
fun toQTagArray() = removeTrailingNullsAndEmptyOthers("q", toTag(), relay)
|
||||
|
||||
fun toNAddr(overrideRelay: String? = relay): String =
|
||||
TlvBuilder()
|
||||
.apply {
|
||||
addString(Nip19Bech32.TlvTypes.SPECIAL, dTag)
|
||||
addStringIfNotNull(Nip19Bech32.TlvTypes.RELAY, relay)
|
||||
addStringIfNotNull(Nip19Bech32.TlvTypes.RELAY, overrideRelay ?: relay)
|
||||
addHex(Nip19Bech32.TlvTypes.AUTHOR, pubKeyHex)
|
||||
addInt(Nip19Bech32.TlvTypes.KIND, kind)
|
||||
}.build()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.encoders
|
||||
|
||||
class Dimension(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
) {
|
||||
fun aspectRatio() = width.toFloat() / height.toFloat()
|
||||
|
||||
fun hasSize() = width > 0 && height > 0
|
||||
|
||||
override fun toString() = "${width}x$height"
|
||||
|
||||
companion object {
|
||||
fun parse(dim: String): Dimension? {
|
||||
if (dim == "0x0") return null
|
||||
|
||||
val parts = dim.split("x")
|
||||
if (parts.size != 2) return null
|
||||
|
||||
return try {
|
||||
val width = parts[0].toInt()
|
||||
val height = parts[1].toInt()
|
||||
|
||||
Dimension(width, height)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.encoders
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||
|
||||
@Immutable
|
||||
data class ETag(
|
||||
val eventId: HexKey,
|
||||
) {
|
||||
var relay: String? = null
|
||||
var authorPubKeyHex: HexKey? = null
|
||||
|
||||
constructor(eventId: HexKey, relayHint: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) {
|
||||
this.relay = relayHint
|
||||
this.authorPubKeyHex = authorPubKeyHex
|
||||
}
|
||||
|
||||
fun countMemory(): Long =
|
||||
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
|
||||
eventId.bytesUsedInMemory() +
|
||||
(relay?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toNEvent(): String = Nip19Bech32.createNEvent(eventId, authorPubKeyHex, null, relay)
|
||||
|
||||
fun toETagArray() = removeTrailingNullsAndEmptyOthers("e", eventId, relay, authorPubKeyHex)
|
||||
|
||||
fun toQTagArray() = removeTrailingNullsAndEmptyOthers("q", eventId, relay, authorPubKeyHex)
|
||||
|
||||
companion object {
|
||||
fun parseNIP19(nevent: String): ETag? {
|
||||
try {
|
||||
val parsed = Nip19Bech32.uriToRoute(nevent)?.entity
|
||||
|
||||
return when (parsed) {
|
||||
is Nip19Bech32.Note -> ETag(parsed.hex)
|
||||
is Nip19Bech32.NEvent -> ETag(parsed.hex, parsed.author, parsed.relay.firstOrNull())
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("PTag", "Issue trying to Decode NIP19 $this: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.encoders
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
|
||||
@Immutable
|
||||
data class EventHint<T : Event>(
|
||||
val event: T,
|
||||
) {
|
||||
var relay: String? = null
|
||||
|
||||
constructor(event: T, relayHint: String? = null) : this(event) {
|
||||
this.relay = relayHint
|
||||
}
|
||||
|
||||
fun countMemory(): Long =
|
||||
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
|
||||
event.countMemory() +
|
||||
(relay?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toNEvent(): String = Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay)
|
||||
|
||||
fun toNPub(): String = Nip19Bech32.createNPub(event.id)
|
||||
|
||||
fun toTagArray(tag: String) = listOfNotNull(tag, event.id, relay, event.pubKey).toTypedArray()
|
||||
|
||||
fun toETagArray() = toTagArray("e")
|
||||
|
||||
fun toQTagArray() = toTagArray("q")
|
||||
}
|
||||
@@ -27,65 +27,53 @@ fun ByteArray.toHexKey(): HexKey = Hex.encode(this)
|
||||
|
||||
fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this)
|
||||
|
||||
object HexValidator {
|
||||
private fun isHexChar(c: Char): Boolean =
|
||||
when (c) {
|
||||
in '0'..'9' -> true
|
||||
in 'a'..'f' -> true
|
||||
in 'A'..'F' -> true
|
||||
else -> false
|
||||
}
|
||||
val lowerCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
|
||||
val upperCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
|
||||
|
||||
val hexToByte: IntArray =
|
||||
IntArray(256) { -1 }.apply {
|
||||
lowerCaseHex.forEachIndexed { index, char -> this[char.code] = index }
|
||||
upperCaseHex.forEachIndexed { index, char -> this[char.code] = index }
|
||||
}
|
||||
|
||||
// Encodes both chars in a single Int variable
|
||||
val byteToHex =
|
||||
IntArray(256) {
|
||||
(lowerCaseHex[(it shr 4)].code shl 8) or lowerCaseHex[(it and 0xF)].code
|
||||
}
|
||||
|
||||
object HexValidator {
|
||||
fun isHex(hex: String?): Boolean {
|
||||
if (hex == null) return false
|
||||
if (hex.isEmpty()) return false
|
||||
if (hex.length % 2 != 0) return false // must be even
|
||||
var isHex = true
|
||||
if (hex.length and 1 != 0) return false // must be even
|
||||
|
||||
for (c in hex) {
|
||||
if (!isHexChar(c)) {
|
||||
isHex = false
|
||||
break
|
||||
}
|
||||
for (c in hex.indices) {
|
||||
if (hexToByte[hex[c].code] < 0) return false
|
||||
}
|
||||
return isHex
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
object Hex {
|
||||
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()
|
||||
require(hex.length and 1 == 0)
|
||||
return ByteArray(hex.length / 2) {
|
||||
(hexToByte[hex[2 * it].code] shl 4 or hexToByte[hex[2 * it + 1].code]).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]
|
||||
val out = CharArray(input.size * 2)
|
||||
var outIdx = 0
|
||||
for (i in 0 until input.size) {
|
||||
val chars = byteToHex[input[i].toInt() and 0xFF]
|
||||
out[outIdx++] = (chars shr 8).toChar()
|
||||
out[outIdx++] = (chars and 0xFF).toChar()
|
||||
}
|
||||
return String(out)
|
||||
}
|
||||
|
||||
@@ -270,6 +270,11 @@ object Nip19Bech32 {
|
||||
}.build()
|
||||
.toNEvent()
|
||||
|
||||
@Deprecated("Use nevent instead")
|
||||
fun createNote(eventId: HexKey): String = eventId.hexToByteArray().toNote()
|
||||
|
||||
fun createNPub(authorPubKeyHex: HexKey): String = authorPubKeyHex.hexToByteArray().toNpub()
|
||||
|
||||
fun createNProfile(
|
||||
authorPubKeyHex: String,
|
||||
relay: List<String>,
|
||||
@@ -299,6 +304,7 @@ fun ByteArray.toNsec() = Bech32.encodeBytes(hrp = "nsec", this, Bech32.Encoding.
|
||||
|
||||
fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
|
||||
|
||||
@Deprecated("Prefer nevent1 instead")
|
||||
fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32)
|
||||
|
||||
fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32)
|
||||
|
||||
@@ -20,43 +20,36 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.encoders
|
||||
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class Nip54InlineMetadata {
|
||||
fun convertFromFileHeader(header: FileHeaderEvent): String? {
|
||||
val myUrl = header.url() ?: return null
|
||||
return createUrl(
|
||||
myUrl,
|
||||
header.tags,
|
||||
fun createUrl(header: IMetaTag): String =
|
||||
createUrl(
|
||||
header.url,
|
||||
header.properties,
|
||||
)
|
||||
}
|
||||
|
||||
fun createUrl(
|
||||
imageUrl: String,
|
||||
tags: Array<Array<String>>,
|
||||
url: String,
|
||||
tags: Map<String, String>,
|
||||
): String {
|
||||
val extension =
|
||||
tags
|
||||
.mapNotNull {
|
||||
if (it.isNotEmpty() && it[0] != "url") {
|
||||
if (it.size > 1) {
|
||||
"${it[0]}=${URLEncoder.encode(it[1], "utf-8")}"
|
||||
} else {
|
||||
"${it[0]}}="
|
||||
}
|
||||
if (it.key != "url") {
|
||||
"${it.key}=${URLEncoder.encode(it.value, "utf-8")}"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.joinToString("&")
|
||||
|
||||
return if (imageUrl.contains("#")) {
|
||||
"$imageUrl&$extension"
|
||||
return if (url.contains("#")) {
|
||||
"$url&$extension"
|
||||
} else {
|
||||
"$imageUrl#$extension"
|
||||
"$url#$extension"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,56 +20,108 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.encoders
|
||||
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.ALT
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.BLUR_HASH
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.DIMENSION
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.FILE_SIZE
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.HASH
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.MAGNET_URI
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.MIME_TYPE
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.ORIGINAL_HASH
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.TORRENT_INFOHASH
|
||||
|
||||
class IMetaTag(
|
||||
val url: String,
|
||||
val properties: Map<String, String>,
|
||||
)
|
||||
|
||||
class IMetaTagBuilder(
|
||||
val url: String,
|
||||
) {
|
||||
val properties = mutableMapOf<String, String>()
|
||||
|
||||
fun add(
|
||||
key: String,
|
||||
value: String,
|
||||
): IMetaTagBuilder {
|
||||
properties.set(key, value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun magnet(uri: String) = add(MAGNET_URI, uri)
|
||||
|
||||
fun mimeType(mime: String) = add(MIME_TYPE, mime)
|
||||
|
||||
fun alt(alt: String) = add(ALT, alt)
|
||||
|
||||
fun hash(hash: HexKey) = add(HASH, hash)
|
||||
|
||||
fun size(size: Int) = add(FILE_SIZE, size.toString())
|
||||
|
||||
fun dims(dims: Dimension) = add(DIMENSION, dims.toString())
|
||||
|
||||
fun blurhash(blurhash: String) = add(BLUR_HASH, blurhash)
|
||||
|
||||
fun originalHash(originalHash: String) = add(ORIGINAL_HASH, originalHash)
|
||||
|
||||
fun torrent(uri: String) = add(TORRENT_INFOHASH, uri)
|
||||
|
||||
fun sensitiveContent(reason: String) = add("content-warning", reason)
|
||||
|
||||
fun build() = IMetaTag(url, properties)
|
||||
}
|
||||
|
||||
class Nip92MediaAttachments {
|
||||
companion object {
|
||||
private const val IMETA = "imeta"
|
||||
}
|
||||
const val IMETA = "imeta"
|
||||
|
||||
fun convertFromFileHeader(header: FileHeaderEvent): Array<String>? {
|
||||
val myUrl = header.url() ?: return null
|
||||
return createTag(
|
||||
myUrl,
|
||||
header.tags,
|
||||
)
|
||||
}
|
||||
fun createTag(header: IMetaTag): Array<String> =
|
||||
createTag(
|
||||
header.url,
|
||||
header.properties,
|
||||
)
|
||||
|
||||
fun createTag(
|
||||
imageUrl: String,
|
||||
tags: Array<Array<String>>,
|
||||
): Array<String> =
|
||||
arrayOf(
|
||||
IMETA,
|
||||
"url $imageUrl",
|
||||
) +
|
||||
tags.mapNotNull {
|
||||
if (it.isNotEmpty() && it[0] != "url") {
|
||||
if (it.size > 1) {
|
||||
"${it[0]} ${it[1]}"
|
||||
fun createTag(
|
||||
url: String,
|
||||
tags: Map<String, String>,
|
||||
): Array<String> =
|
||||
arrayOf(
|
||||
IMETA,
|
||||
"url $url",
|
||||
) +
|
||||
tags.mapNotNull {
|
||||
if (it.key != "url") {
|
||||
"${it.key} ${it.value}"
|
||||
} else {
|
||||
"${it[0]}}"
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun parse(
|
||||
url: String,
|
||||
tags: Array<Array<String>>,
|
||||
): Map<String, String> =
|
||||
tags
|
||||
.firstOrNull {
|
||||
it.size > 1 && it[0] == IMETA && it[1] == "url $url"
|
||||
}?.let { tagList ->
|
||||
parseIMeta(tagList)
|
||||
} ?: emptyMap()
|
||||
|
||||
fun parse(tags: Array<Array<String>>): Map<String, Map<String, String>> =
|
||||
tags.filter { it.size > 1 && it[0] == IMETA }.associate {
|
||||
val allTags = parseIMeta(it)
|
||||
(allTags.get("url") ?: "") to allTags
|
||||
}
|
||||
|
||||
fun parse(
|
||||
imageUrl: String,
|
||||
tags: Array<Array<String>>,
|
||||
): Map<String, String> =
|
||||
tags
|
||||
.firstOrNull {
|
||||
it.size > 1 && it[0] == IMETA && it[1] == "url $imageUrl"
|
||||
}?.let { tagList ->
|
||||
tagList.associate { tag ->
|
||||
val parts = tag.split(" ", limit = 2)
|
||||
when (parts.size) {
|
||||
2 -> parts[0] to parts[1]
|
||||
1 -> parts[0] to ""
|
||||
else -> "" to ""
|
||||
}
|
||||
private fun parseIMeta(tags: Array<String>): Map<String, String> =
|
||||
tags.associate { tag ->
|
||||
val parts = tag.split(" ", limit = 2)
|
||||
when (parts.size) {
|
||||
2 -> parts[0] to parts[1]
|
||||
1 -> parts[0] to ""
|
||||
else -> "" to ""
|
||||
}
|
||||
} ?: emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.encoders
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||
|
||||
@Immutable
|
||||
data class PTag(
|
||||
val pubKeyHex: HexKey,
|
||||
) {
|
||||
var relay: String? = null
|
||||
|
||||
constructor(pubKeyHex: HexKey, relayHint: String?) : this(pubKeyHex) {
|
||||
this.relay = relayHint?.ifBlank { null }
|
||||
}
|
||||
|
||||
fun countMemory(): Long =
|
||||
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
|
||||
pubKeyHex.bytesUsedInMemory() +
|
||||
(relay?.bytesUsedInMemory() ?: 0)
|
||||
|
||||
fun toNProfile(): String = Nip19Bech32.createNProfile(pubKeyHex, relay?.let { listOf(it) } ?: emptyList())
|
||||
|
||||
fun toNPub(): String = Nip19Bech32.createNPub(pubKeyHex)
|
||||
|
||||
fun toPTagArray() = removeTrailingNullsAndEmptyOthers("p", pubKeyHex, relay)
|
||||
|
||||
companion object {
|
||||
fun parseNAddr(nprofile: String): PTag? {
|
||||
try {
|
||||
val parsed = Nip19Bech32.uriToRoute(nprofile)?.entity
|
||||
|
||||
return when (parsed) {
|
||||
is Nip19Bech32.NPub -> PTag(parsed.hex)
|
||||
is Nip19Bech32.NProfile -> PTag(parsed.hex, parsed.relay.firstOrNull())
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("PTag", "Issue trying to Decode NIP19 $this: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ class AppMetadata {
|
||||
var website: String? = null
|
||||
var about: String? = null
|
||||
var subscription: Boolean? = false
|
||||
var cashuAccepted: Boolean? = false
|
||||
var encryptionSupported: Boolean? = false
|
||||
var acceptsNutZaps: Boolean? = false
|
||||
var supportsEncryption: Boolean? = false
|
||||
var personalized: Boolean? = false
|
||||
var amount: String? = null
|
||||
|
||||
@@ -71,8 +71,8 @@ class AppMetadata {
|
||||
(website?.bytesUsedInMemory() ?: 0L) +
|
||||
(about?.bytesUsedInMemory() ?: 0L) +
|
||||
(subscription?.bytesUsedInMemory() ?: 0L) +
|
||||
(cashuAccepted?.bytesUsedInMemory() ?: 0L) +
|
||||
(encryptionSupported?.bytesUsedInMemory() ?: 0L) +
|
||||
(acceptsNutZaps?.bytesUsedInMemory() ?: 0L) +
|
||||
(supportsEncryption?.bytesUsedInMemory() ?: 0L) +
|
||||
(personalized?.bytesUsedInMemory() ?: 0L) + // A Boolean has 8 bytes of header, plus 1 byte of payload, for a total of 9 bytes of information. The JVM then rounds it up to the next multiple of 8. so the one instance of java.lang.Boolean takes up 16 bytes of memory.
|
||||
(amount?.bytesUsedInMemory() ?: 0L) +
|
||||
(nip05?.bytesUsedInMemory() ?: 0L) +
|
||||
@@ -95,7 +95,7 @@ class AppMetadata {
|
||||
|
||||
fun nip05(): String? = nip05
|
||||
|
||||
fun profilePicture(): String? = picture
|
||||
fun profilePicture(): String? = picture ?: image
|
||||
|
||||
fun cleanBlankNames() {
|
||||
if (picture?.isNotEmpty() == true) picture = picture?.trim()
|
||||
|
||||
@@ -57,21 +57,15 @@ open class BaseTextNoteEvent(
|
||||
|
||||
fun isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) }
|
||||
|
||||
open fun replyTos(): List<HexKey> {
|
||||
val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] }
|
||||
open fun markedReplyTos(): List<HexKey> {
|
||||
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
|
||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
|
||||
|
||||
val newStyleReplyTos = listOfNotNull(newStyleReply, newStyleRoot)
|
||||
|
||||
return if (newStyleReplyTos.isNotEmpty()) {
|
||||
newStyleReplyTos
|
||||
} else {
|
||||
oldStylePositional
|
||||
}
|
||||
return listOfNotNull(newStyleReply, newStyleRoot)
|
||||
}
|
||||
|
||||
fun replyingTo(): HexKey? {
|
||||
open fun unMarkedReplyTos(): List<HexKey> = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] }
|
||||
|
||||
open fun replyingTo(): HexKey? {
|
||||
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "e" }?.get(1)
|
||||
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
|
||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
|
||||
@@ -79,7 +73,7 @@ open class BaseTextNoteEvent(
|
||||
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
||||
}
|
||||
|
||||
fun replyingToAddress(): ATag? {
|
||||
open fun replyingToAddress(): ATag? {
|
||||
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it[2]) }
|
||||
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "reply" }?.let { ATag.parseAtag(it[1], it[2]) }
|
||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }?.let { ATag.parseAtag(it[1], it[2]) }
|
||||
@@ -87,7 +81,7 @@ open class BaseTextNoteEvent(
|
||||
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
||||
}
|
||||
|
||||
fun replyingToAddressOrEvent(): String? {
|
||||
open fun replyingToAddressOrEvent(): String? {
|
||||
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && (it[0] == "e" || it[0] == "a") }?.get(1)
|
||||
val newStyleReply = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "reply" }?.get(1)
|
||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "root" }?.get(1)
|
||||
@@ -190,21 +184,33 @@ open class BaseTextNoteEvent(
|
||||
}
|
||||
|
||||
fun tagsWithoutCitations(): List<String> {
|
||||
val repliesTo = replyTos()
|
||||
val certainRepliesTo = markedReplyTos()
|
||||
val uncertainRepliesTo = unMarkedReplyTos()
|
||||
|
||||
val tagAddresses =
|
||||
taggedAddresses()
|
||||
.filter {
|
||||
it.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || it.kind != WikiNoteEvent.KIND)
|
||||
// removes forks from itself.
|
||||
}.map { it.toTag() }
|
||||
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
||||
|
||||
if (certainRepliesTo.isEmpty() && uncertainRepliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
||||
|
||||
val citations = findCitations()
|
||||
|
||||
return if (citations.isEmpty()) {
|
||||
repliesTo + tagAddresses
|
||||
if (certainRepliesTo.isNotEmpty()) {
|
||||
certainRepliesTo + tagAddresses
|
||||
} else {
|
||||
uncertainRepliesTo + tagAddresses
|
||||
}
|
||||
} else {
|
||||
repliesTo.filter { it !in citations }
|
||||
if (certainRepliesTo.isNotEmpty()) {
|
||||
certainRepliesTo + tagAddresses.filter { it !in citations }
|
||||
} else {
|
||||
// mix bag between `e` for replies and `e` for citations
|
||||
uncertainRepliesTo.filter { it !in citations }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class BlossomAuthorizationEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
companion object {
|
||||
const val KIND = 24242
|
||||
|
||||
fun createGetAuth(
|
||||
hash: HexKey,
|
||||
alt: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomAuthorizationEvent) -> Unit,
|
||||
) = createAuth("get", hash, null, alt, signer, createdAt, onReady)
|
||||
|
||||
fun createListAuth(
|
||||
signer: NostrSigner,
|
||||
alt: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomAuthorizationEvent) -> Unit,
|
||||
) = createAuth("list", null, null, alt, signer, createdAt, onReady)
|
||||
|
||||
fun createDeleteAuth(
|
||||
hash: HexKey,
|
||||
alt: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomAuthorizationEvent) -> Unit,
|
||||
) = createAuth("delete", hash, null, alt, signer, createdAt, onReady)
|
||||
|
||||
fun createUploadAuth(
|
||||
hash: HexKey,
|
||||
size: Long,
|
||||
alt: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomAuthorizationEvent) -> Unit,
|
||||
) = createAuth("upload", hash, size, alt, signer, createdAt, onReady)
|
||||
|
||||
private fun createAuth(
|
||||
type: String,
|
||||
hash: HexKey?,
|
||||
fileSize: Long?,
|
||||
alt: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomAuthorizationEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
listOfNotNull(
|
||||
arrayOf("t", type),
|
||||
arrayOf("expiration", TimeUtils.oneHourAhead().toString()),
|
||||
fileSize?.let { arrayOf("size", it.toString()) },
|
||||
hash?.let { arrayOf("x", it) },
|
||||
)
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), alt, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class BlossomServersEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun servers(): List<String> =
|
||||
tags.mapNotNull {
|
||||
if (it.size > 1 && it[0] == "server") {
|
||||
it[1]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10063
|
||||
const val FIXED_D_TAG = ""
|
||||
const val ALT = "File servers used by the author"
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = ATag.assembleATag(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createTagArray(servers: List<String>): Array<Array<String>> =
|
||||
servers
|
||||
.map {
|
||||
arrayOf("server", it)
|
||||
}.plusElement(arrayOf("alt", ALT))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: BlossomServersEvent,
|
||||
relays: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomServersEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter { it[0] != "server" }
|
||||
.plus(
|
||||
relays.map {
|
||||
arrayOf("server", it)
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomServersEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
servers: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlossomServersEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(servers), "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -40,10 +41,9 @@ class ChannelMessageEvent(
|
||||
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) }
|
||||
override fun markedReplyTos() = super.markedReplyTos().filter { it != channel() }
|
||||
|
||||
override fun unMarkedReplyTos() = super.unMarkedReplyTos().filter { it != channel() }
|
||||
|
||||
companion object {
|
||||
const val KIND = 42
|
||||
@@ -59,8 +59,9 @@ class ChannelMessageEvent(
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
directMentions: Set<HexKey> = emptySet(),
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (ChannelMessageEvent) -> Unit,
|
||||
) {
|
||||
@@ -68,8 +69,14 @@ class ChannelMessageEvent(
|
||||
mutableListOf(
|
||||
arrayOf("e", channel, "", "root"),
|
||||
)
|
||||
replyTos?.forEach { tags.add(arrayOf("e", it)) }
|
||||
mentions?.forEach { tags.add(arrayOf("p", it)) }
|
||||
replyTos?.forEach {
|
||||
if (it in directMentions) {
|
||||
tags.add(arrayOf("q", it))
|
||||
} else {
|
||||
tags.add(arrayOf("e", it))
|
||||
}
|
||||
}
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
}
|
||||
@@ -78,12 +85,8 @@ class ChannelMessageEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
tags.add(
|
||||
arrayOf("alt", ALT),
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Immutable
|
||||
class ChatMessageEncryptedFileHeaderEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
ChatroomKeyable,
|
||||
NIP17Group {
|
||||
/** Recipients intended to receive this conversation */
|
||||
fun recipientsPubKey() = tags.mapNotNull { if (it.size > 1 && it[0] == "p") it[1] else null }
|
||||
|
||||
override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet()
|
||||
|
||||
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 = ChatroomKey(talkingWith(toRemove).toImmutableSet())
|
||||
|
||||
fun url() = content
|
||||
|
||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
|
||||
fun algo() = tags.firstOrNull { it.size > 1 && it[0] == ENCRYPTION_ALGORITHM }?.get(1)
|
||||
|
||||
fun key() =
|
||||
tags
|
||||
.firstOrNull { it.size > 1 && it[0] == ENCRYPTION_KEY }
|
||||
?.get(1)
|
||||
?.runCatching { this.hexToByteArray() }
|
||||
?.getOrNull()
|
||||
|
||||
fun nonce() =
|
||||
tags
|
||||
.firstOrNull { it.size > 1 && it[0] == ENCRYPTION_NONCE }
|
||||
?.get(1)
|
||||
?.runCatching { this.hexToByteArray() }
|
||||
?.getOrNull()
|
||||
|
||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||
|
||||
fun originalHash() = tags.firstOrNull { it.size > 1 && it[0] == ORIGINAL_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)?.let { Dimension.parse(it) }
|
||||
|
||||
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
||||
|
||||
companion object {
|
||||
const val KIND = 15
|
||||
const val ALT_DESCRIPTION = "Encrypted file in chat"
|
||||
|
||||
const val MIME_TYPE = "file-type"
|
||||
|
||||
const val ENCRYPTION_ALGORITHM = "encryption-algorithm"
|
||||
const val ENCRYPTION_KEY = "decryption-key"
|
||||
const val ENCRYPTION_NONCE = "decryption-nonce"
|
||||
|
||||
const val FILE_SIZE = "size"
|
||||
const val DIMENSION = "dim"
|
||||
const val BLUR_HASH = "blurhash"
|
||||
const val HASH = "x"
|
||||
const val ORIGINAL_HASH = "ox"
|
||||
|
||||
const val ALT = "alt"
|
||||
|
||||
fun buildTags(
|
||||
to: List<HexKey>,
|
||||
repliesTo: List<HexKey>? = null,
|
||||
contentType: String?,
|
||||
algo: String,
|
||||
key: ByteArray,
|
||||
nonce: ByteArray? = null,
|
||||
originalHash: String? = null,
|
||||
hash: String? = null,
|
||||
size: Int? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
alt: String?,
|
||||
): Array<Array<String>> {
|
||||
val repliesHex = repliesTo?.map { arrayOf("e", it) } ?: emptyList()
|
||||
|
||||
return (
|
||||
to.map { arrayOf("p", it) } + repliesHex +
|
||||
listOfNotNull(
|
||||
contentType?.let { arrayOf(MIME_TYPE, it) },
|
||||
arrayOf(ENCRYPTION_ALGORITHM, algo),
|
||||
arrayOf(ENCRYPTION_KEY, key.toHexKey()),
|
||||
nonce?.let { arrayOf(ENCRYPTION_NONCE, it.toHexKey()) },
|
||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf(ALT, ALT_DESCRIPTION),
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it.toString()) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it.toString()) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
).toTypedArray()
|
||||
}
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
to: List<HexKey>,
|
||||
repliesTo: List<HexKey>? = null,
|
||||
contentType: String?,
|
||||
algo: String,
|
||||
key: ByteArray,
|
||||
nonce: ByteArray? = null,
|
||||
originalHash: String? = null,
|
||||
hash: String? = null,
|
||||
size: Int? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
alt: String?,
|
||||
signer: NostrSigner,
|
||||
isDraft: Boolean,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChatMessageEncryptedFileHeaderEvent) -> Unit,
|
||||
) {
|
||||
val tags = buildTags(to, repliesTo, contentType, algo, key, nonce, originalHash, hash, size, dimensions, blurhash, sensitiveContent, alt)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags, url, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags, url, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -37,7 +38,8 @@ class ChatMessageEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
ChatroomKeyable {
|
||||
ChatroomKeyable,
|
||||
NIP17Group {
|
||||
/** Recipients intended to receive this conversation */
|
||||
fun recipientsPubKey() = tags.mapNotNull { if (it.size > 1 && it[0] == "p") it[1] else null }
|
||||
|
||||
@@ -61,6 +63,8 @@ class ChatMessageEvent(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet()
|
||||
|
||||
override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(talkingWith(toRemove).toImmutableSet())
|
||||
|
||||
companion object {
|
||||
@@ -79,7 +83,7 @@ class ChatMessageEvent(
|
||||
geohash: String? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (ChatMessageEvent) -> Unit,
|
||||
) {
|
||||
@@ -96,12 +100,8 @@ class ChatMessageEvent(
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
subject?.let { tags.add(arrayOf("subject", it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
// tags.add(arrayOf("alt", alt))
|
||||
|
||||
@@ -114,6 +114,10 @@ class ChatMessageEvent(
|
||||
}
|
||||
}
|
||||
|
||||
interface NIP17Group {
|
||||
fun groupMembers(): Set<HexKey>
|
||||
}
|
||||
|
||||
interface ChatroomKeyable {
|
||||
fun chatroomKey(toRemove: HexKey): ChatroomKey
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@@ -112,7 +114,7 @@ class ClassifiedsEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
@@ -188,10 +190,8 @@ class ClassifiedsEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
// tags.add(arrayOf("nip94", it.toJson()))
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.ETag
|
||||
import com.vitorpamplona.quartz.encoders.EventHint
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.encoders.PTag
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||
|
||||
@Immutable
|
||||
class CommentEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
fun root() = tags.firstOrNull { it.size > 3 && it[3] == "root" }?.get(1)
|
||||
|
||||
fun getRootScopes() = tags.filter { it.size > 1 && it[0] == "I" || it[0] == "A" || it[0] == "E" }
|
||||
|
||||
fun getRootKinds() = tags.filter { it.size > 1 && it[0] == "K" }
|
||||
|
||||
fun getDirectReplies() = tags.filter { it.size > 1 && it[0] == "i" || it[0] == "a" || it[0] == "e" }
|
||||
|
||||
fun getDirectKinds() = tags.filter { it.size > 1 && it[0] == "k" }
|
||||
|
||||
fun isGeohashTag(tag: Array<String>) = tag.size > 1 && (tag[0] == "i" || tag[0] == "I") && tag[1].startsWith("geo:")
|
||||
|
||||
private fun getGeoHashList() = tags.filter { isGeohashTag(it) }
|
||||
|
||||
override fun hasGeohashes() = tags.any { isGeohashTag(it) }
|
||||
|
||||
override fun geohashes() = getGeoHashList().map { it[1].drop(4).lowercase() }
|
||||
|
||||
override fun getGeoHash(): String? = geohashes().maxByOrNull { it.length }
|
||||
|
||||
override fun isTaggedGeoHash(hashtag: String) = tags.any { isGeohashTag(it) && it[1].endsWith(hashtag, true) }
|
||||
|
||||
override fun isTaggedGeoHashes(hashtags: Set<String>) = geohashes().any { it in hashtags }
|
||||
|
||||
override fun markedReplyTos(): List<HexKey> = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + tags.filter { it.size > 1 && it[0] == "E" }.map { it[1] }
|
||||
|
||||
override fun unMarkedReplyTos() = emptyList<String>()
|
||||
|
||||
override fun replyingTo(): HexKey? =
|
||||
tags.lastOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
?: tags.lastOrNull { it.size > 1 && it[0] == "E" }?.get(1)
|
||||
|
||||
override fun replyingToAddress(): ATag? =
|
||||
tags.lastOrNull { it.size > 1 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it.getOrNull(2)) }
|
||||
?: tags.lastOrNull { it.size > 1 && it[0] == "A" }?.let { ATag.parseAtag(it[1], it.getOrNull(2)) }
|
||||
|
||||
override fun replyingToAddressOrEvent(): HexKey? = replyingToAddress()?.toTag() ?: replyingTo()
|
||||
|
||||
companion object {
|
||||
const val KIND = 1111
|
||||
|
||||
fun rootGeohashMipMap(geohash: String): Array<Array<String>> =
|
||||
geohash.indices
|
||||
.asSequence()
|
||||
.map { arrayOf("I", "geo:" + geohash.substring(0, it + 1)) }
|
||||
.toList()
|
||||
.reversed()
|
||||
.toTypedArray()
|
||||
|
||||
fun firstReplyToEvent(
|
||||
msg: String,
|
||||
replyingTo: EventHint<Event>,
|
||||
usersMentioned: Set<PTag> = emptySet(),
|
||||
addressesMentioned: Set<ATag> = emptySet(),
|
||||
eventsMentioned: Set<ETag> = emptySet(),
|
||||
imetas: List<IMetaTag>? = null,
|
||||
geohash: String? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
isDraft: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CommentEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
if (replyingTo.event is AddressableEvent) {
|
||||
tags.add(removeTrailingNullsAndEmptyOthers("A", replyingTo.event.addressTag(), replyingTo.relay))
|
||||
tags.add(removeTrailingNullsAndEmptyOthers("a", replyingTo.event.addressTag(), replyingTo.relay))
|
||||
}
|
||||
|
||||
tags.add(removeTrailingNullsAndEmptyOthers("E", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
|
||||
tags.add(arrayOf("K", "${replyingTo.event.kind}"))
|
||||
|
||||
tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
|
||||
tags.add(arrayOf("k", "${replyingTo.event.kind}"))
|
||||
|
||||
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun replyComment(
|
||||
msg: String,
|
||||
replyingTo: EventHint<CommentEvent>,
|
||||
usersMentioned: Set<PTag> = emptySet(),
|
||||
addressesMentioned: Set<ATag> = emptySet(),
|
||||
eventsMentioned: Set<ETag> = emptySet(),
|
||||
imetas: List<IMetaTag>? = null,
|
||||
geohash: String? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
isDraft: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CommentEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
tags.addAll(replyingTo.event.getRootScopes())
|
||||
tags.addAll(replyingTo.event.getRootKinds())
|
||||
|
||||
tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
|
||||
tags.add(arrayOf("k", "${replyingTo.event.kind}"))
|
||||
|
||||
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun createGeoComment(
|
||||
msg: String,
|
||||
geohash: String? = null,
|
||||
usersMentioned: Set<PTag> = emptySet(),
|
||||
addressesMentioned: Set<ATag> = emptySet(),
|
||||
eventsMentioned: Set<ETag> = emptySet(),
|
||||
imetas: List<IMetaTag>? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
isDraft: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CommentEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
geohash?.let { tags.addAll(rootGeohashMipMap(it)) }
|
||||
tags.add(arrayOf("K", "geo"))
|
||||
|
||||
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, null, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
private fun create(
|
||||
msg: String,
|
||||
tags: MutableList<Array<String>>,
|
||||
usersMentioned: Set<PTag> = emptySet(),
|
||||
addressesMentioned: Set<ATag> = emptySet(),
|
||||
eventsMentioned: Set<ETag> = emptySet(),
|
||||
imetas: List<IMetaTag>? = null,
|
||||
geohash: String? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
isDraft: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CommentEvent) -> Unit,
|
||||
) {
|
||||
usersMentioned.forEach { tags.add(it.toPTagArray()) }
|
||||
addressesMentioned.forEach { tags.add(it.toQTagArray()) }
|
||||
eventsMentioned.forEach { tags.add(it.toQTagArray()) }
|
||||
|
||||
findHashtags(msg).forEach {
|
||||
val lowercaseTag = it.lowercase()
|
||||
tags.add(arrayOf("t", it))
|
||||
if (it != lowercaseTag) {
|
||||
tags.add(arrayOf("t", it.lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
findURLs(msg).forEach { tags.add(arrayOf("r", it)) }
|
||||
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(arrayOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,6 @@ class ContactListEvent(
|
||||
|
||||
fun unverifiedFollowGeohashSet() = tags.filter { it.size > 1 && it[0] == "g" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
fun unverifiedFollowAddressSet() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
fun follows() =
|
||||
tags.mapNotNull {
|
||||
try {
|
||||
@@ -141,21 +139,11 @@ class ContactListEvent(
|
||||
val tags =
|
||||
listOf(arrayOf("alt", ALT)) +
|
||||
followUsers.map {
|
||||
if (it.relayUri != null) {
|
||||
arrayOf("p", it.pubKeyHex, it.relayUri)
|
||||
} else {
|
||||
arrayOf("p", it.pubKeyHex)
|
||||
}
|
||||
listOfNotNull("p", it.pubKeyHex, it.relayUri).toTypedArray()
|
||||
} +
|
||||
followTags.map { arrayOf("t", it) } +
|
||||
followEvents.map { arrayOf("e", it) } +
|
||||
followCommunities.map {
|
||||
if (it.relay != null) {
|
||||
arrayOf("a", it.toTag(), it.relay)
|
||||
} else {
|
||||
arrayOf("a", it.toTag())
|
||||
}
|
||||
} +
|
||||
followCommunities.map { it.toATagArray() } +
|
||||
followGeohashes.map { arrayOf("g", it) }
|
||||
|
||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
@@ -189,13 +177,7 @@ class ContactListEvent(
|
||||
} +
|
||||
followTags.map { arrayOf("t", it) } +
|
||||
followEvents.map { arrayOf("e", it) } +
|
||||
followCommunities.map {
|
||||
if (it.relay != null) {
|
||||
arrayOf("a", it.toTag(), it.relay)
|
||||
} else {
|
||||
arrayOf("a", it.toTag())
|
||||
}
|
||||
} +
|
||||
followCommunities.map { it.toATagArray() } +
|
||||
followGeohashes.map { arrayOf("g", it) }
|
||||
|
||||
return create(
|
||||
@@ -452,6 +434,7 @@ class UserMetadata {
|
||||
var website: String? = null
|
||||
var about: String? = null
|
||||
var bot: Boolean? = null
|
||||
var pronouns: String? = null
|
||||
|
||||
var nip05: String? = null
|
||||
var nip05Verified: Boolean = false
|
||||
@@ -482,6 +465,8 @@ class UserMetadata {
|
||||
fun profilePicture(): String? = picture
|
||||
|
||||
fun cleanBlankNames() {
|
||||
if (pronouns == "null") pronouns = null
|
||||
|
||||
if (picture?.isNotEmpty() == true) picture = picture?.trim()
|
||||
if (nip05?.isNotEmpty() == true) nip05 = nip05?.trim()
|
||||
if (displayName?.isNotEmpty() == true) displayName = displayName?.trim()
|
||||
@@ -489,6 +474,7 @@ class UserMetadata {
|
||||
if (username?.isNotEmpty() == true) username = username?.trim()
|
||||
if (lud06?.isNotEmpty() == true) lud06 = lud06?.trim()
|
||||
if (lud16?.isNotEmpty() == true) lud16 = lud16?.trim()
|
||||
if (pronouns?.isNotEmpty() == true) pronouns = pronouns?.trim()
|
||||
|
||||
if (banner?.isNotEmpty() == true) banner = banner?.trim()
|
||||
if (website?.isNotEmpty() == true) website = website?.trim()
|
||||
@@ -505,6 +491,7 @@ class UserMetadata {
|
||||
if (banner?.isBlank() == true) banner = null
|
||||
if (website?.isBlank() == true) website = null
|
||||
if (domain?.isBlank() == true) domain = null
|
||||
if (pronouns?.isBlank() == true) pronouns = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,17 @@ class DraftEvent(
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: InteractiveStoryBaseEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: LiveActivitiesChatMessageEvent,
|
||||
@@ -188,6 +199,18 @@ class DraftEvent(
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: CommentEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tagsWithMarkers = originalNote.getRootScopes() + originalNote.getDirectReplies()
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: TextNoteEvent,
|
||||
|
||||
@@ -33,7 +33,6 @@ 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.addDeserializer
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
@@ -50,6 +49,8 @@ import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import com.vitorpamplona.quartz.utils.remove
|
||||
import com.vitorpamplona.quartz.utils.startsWith
|
||||
import java.math.BigDecimal
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -280,7 +281,13 @@ open class Event(
|
||||
return PoWRank.getCommited(id, commitedPoW)
|
||||
}
|
||||
|
||||
override fun getGeoHash(): String? = tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
|
||||
override fun getGeoHash(): String? =
|
||||
tags
|
||||
.filter { it.size > 1 && it[0] == "g" }
|
||||
.maxByOrNull {
|
||||
it[1].length
|
||||
}?.get(1)
|
||||
?.ifBlank { null }
|
||||
|
||||
override fun getReward(): BigDecimal? =
|
||||
try {
|
||||
@@ -289,6 +296,8 @@ open class Event(
|
||||
null
|
||||
}
|
||||
|
||||
fun filterTags(startsWith: Array<String>) = tags.remove(startsWith)
|
||||
|
||||
open fun toNIP19(): String =
|
||||
if (this is AddressableEvent) {
|
||||
ATag(kind, pubKey, dTag(), null).toNAddr()
|
||||
@@ -550,7 +559,7 @@ class HostStub(
|
||||
interface AddressableEvent {
|
||||
fun dTag(): String
|
||||
|
||||
fun address(): ATag
|
||||
fun address(relayHint: String? = null): ATag
|
||||
|
||||
fun addressTag(): String
|
||||
}
|
||||
@@ -568,7 +577,7 @@ open class BaseAddressableEvent(
|
||||
AddressableEvent {
|
||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
|
||||
|
||||
/**
|
||||
* Creates the tag in a memory effecient way (without creating the ATag class
|
||||
@@ -582,3 +591,5 @@ data class ZapSplitSetup(
|
||||
val weight: Double,
|
||||
val isLnAddress: Boolean,
|
||||
)
|
||||
|
||||
interface RootScope
|
||||
|
||||
@@ -20,15 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.nip46.NostrConnectEvent
|
||||
|
||||
class EventFactory {
|
||||
companion object {
|
||||
val additionalFactories =
|
||||
mutableMapOf(
|
||||
WikiNoteEvent.KIND to ::WikiNoteEvent,
|
||||
)
|
||||
val factories: MutableMap<Int, (HexKey, HexKey, Long, Array<Array<String>>, String, HexKey) -> Event> = mutableMapOf()
|
||||
|
||||
fun create(
|
||||
id: String,
|
||||
@@ -48,6 +46,8 @@ class EventFactory {
|
||||
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)
|
||||
BlossomServersEvent.KIND -> BlossomServersEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BlossomAuthorizationEvent.KIND -> BlossomAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -59,6 +59,20 @@ class EventFactory {
|
||||
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)
|
||||
ChatMessageEncryptedFileHeaderEvent.KIND -> {
|
||||
if (id.isBlank()) {
|
||||
ChatMessageEncryptedFileHeaderEvent(
|
||||
Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey(),
|
||||
pubKey,
|
||||
createdAt,
|
||||
tags,
|
||||
content,
|
||||
sig,
|
||||
)
|
||||
} else {
|
||||
ChatMessageEncryptedFileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
}
|
||||
}
|
||||
ChatMessageEvent.KIND -> {
|
||||
if (id.isBlank()) {
|
||||
ChatMessageEvent(
|
||||
@@ -75,6 +89,7 @@ class EventFactory {
|
||||
}
|
||||
ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommentEvent.KIND -> CommentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -98,6 +113,9 @@ class EventFactory {
|
||||
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -117,12 +135,14 @@ class EventFactory {
|
||||
NIP90UserDiscoveryResponseEvent.KIND -> NIP90UserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PictureEvent.KIND -> PictureEvent(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)
|
||||
PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RecommendRelayEvent.KIND -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelationshipStatusEvent.KIND -> RelationshipStatusEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelaySetEvent.KIND -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReportEvent.KIND -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -139,7 +159,7 @@ class EventFactory {
|
||||
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
else -> {
|
||||
additionalFactories[kind]?.let {
|
||||
factories[kind]?.let {
|
||||
return it(id, pubKey, createdAt, tags, content, sig)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -38,8 +39,6 @@ class FileHeaderEvent(
|
||||
|
||||
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[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)
|
||||
@@ -48,7 +47,7 @@ class FileHeaderEvent(
|
||||
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
|
||||
|
||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||
|
||||
@@ -76,6 +75,41 @@ class FileHeaderEvent(
|
||||
const val ORIGINAL_HASH = "ox"
|
||||
const val ALT = "alt"
|
||||
|
||||
fun buildTags(
|
||||
url: String,
|
||||
magnetUri: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
): Array<Array<String>> =
|
||||
listOfNotNull(
|
||||
arrayOf(URL, url),
|
||||
magnetUri?.let { arrayOf(MAGNET_URI, it) },
|
||||
mimeType?.let { arrayOf(MIME_TYPE, it) },
|
||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it.toString()) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
magnetUri: String? = null,
|
||||
@@ -83,47 +117,20 @@ class FileHeaderEvent(
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FileHeaderEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
listOfNotNull(
|
||||
arrayOf(URL, url),
|
||||
magnetUri?.let { arrayOf(MAGNET_URI, it) },
|
||||
mimeType?.let { arrayOf(MIME_TYPE, it) },
|
||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
val tags = buildTags(url, magnetUri, mimeType, alt, hash, size, dimensions, blurhash, originalHash, magnetURI, torrentInfoHash, sensitiveContent)
|
||||
|
||||
val content = alt ?: ""
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AESGCM(
|
||||
val key: String,
|
||||
val nonce: String,
|
||||
)
|
||||
|
||||
@@ -40,8 +40,6 @@ class FileStorageEvent(
|
||||
|
||||
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? =
|
||||
try {
|
||||
Base64.getDecoder().decode(content)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -36,8 +37,6 @@ class FileStorageHeaderEvent(
|
||||
) : 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)
|
||||
@@ -46,7 +45,7 @@ class FileStorageHeaderEvent(
|
||||
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
|
||||
|
||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||
|
||||
@@ -76,11 +75,10 @@ class FileStorageHeaderEvent(
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
@@ -93,11 +91,10 @@ class FileStorageHeaderEvent(
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
alt?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it.toString()) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
|
||||
@@ -20,14 +20,10 @@
|
||||
*/
|
||||
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.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import java.util.HashSet
|
||||
@@ -41,15 +37,7 @@ abstract class GeneralListEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
fun category() = dTag()
|
||||
|
||||
fun bookmarkedPosts() = taggedEvents()
|
||||
@@ -64,8 +52,6 @@ abstract class GeneralListEvent(
|
||||
|
||||
fun nameOrTitle() = name()?.ifBlank { null } ?: title()?.ifBlank { null }
|
||||
|
||||
fun cachedPrivateTags(): Array<Array<String>>? = privateTagsCache
|
||||
|
||||
fun filterTagList(
|
||||
key: String,
|
||||
privateTags: Array<Array<String>>?,
|
||||
@@ -95,30 +81,6 @@ abstract class GeneralListEvent(
|
||||
onReady(isTagged(key, tag))
|
||||
}
|
||||
|
||||
fun privateTags(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) {
|
||||
onReady(emptyArray())
|
||||
return
|
||||
}
|
||||
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
signer.decrypt(content, pubKey) {
|
||||
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
|
||||
privateTagsCache?.let { onReady(it) }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun privateTagsOrEmpty(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
|
||||
@@ -77,13 +77,7 @@ class GiftWrapEvent(
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
plainContent(signer) { giftStr ->
|
||||
val gift =
|
||||
try {
|
||||
fromJson(giftStr)
|
||||
} catch (e: Exception) {
|
||||
Log.w("GiftWrapEvent", "Couldn't Parse the content " + this.toNostrUri() + " " + giftStr)
|
||||
return@plainContent
|
||||
}
|
||||
val gift = fromJson(giftStr)
|
||||
|
||||
if (gift is WrappedEvent) {
|
||||
gift.host = HostStub(this.id, this.pubKey, this.kind)
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -90,7 +91,7 @@ class GitReplyEvent(
|
||||
root: String? = null,
|
||||
directMentions: Set<HexKey> = emptySet(),
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
forkedFrom: Event? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
@@ -148,12 +149,8 @@ class GitReplyEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
tags.add(arrayOf("alt", "a git issue reply"))
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
|
||||
open class InteractiveStoryBaseEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
fun title() = firstTag("title")
|
||||
|
||||
fun summary() = firstTag("summary")
|
||||
|
||||
fun image() = firstTag("image")
|
||||
|
||||
fun options() =
|
||||
tags
|
||||
.filter { it.size > 2 && it[0] == "option" }
|
||||
.mapNotNull { ATag.parse(it[2], it.getOrNull(3))?.let { aTag -> StoryOption(it[1], aTag) } }
|
||||
|
||||
companion object {
|
||||
fun generalTags(
|
||||
content: String,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
): Array<Array<String>> {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
findHashtags(content).forEach {
|
||||
val lowercaseTag = it.lowercase()
|
||||
tags.add(arrayOf("t", it))
|
||||
if (it != lowercaseTag) {
|
||||
tags.add(arrayOf("t", it.lowercase()))
|
||||
}
|
||||
}
|
||||
findURLs(content).forEach { tags.add(arrayOf("r", it)) }
|
||||
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(arrayOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
return tags.toTypedArray()
|
||||
}
|
||||
|
||||
fun makeTags(
|
||||
baseId: String,
|
||||
alt: String,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
image: String? = null,
|
||||
options: List<StoryOption> = emptyList(),
|
||||
): Array<Array<String>> =
|
||||
(
|
||||
listOfNotNull(
|
||||
arrayOf("d", baseId),
|
||||
arrayOf("title", title),
|
||||
summary?.let { arrayOf("summary", it) },
|
||||
image?.let { arrayOf("image", it) },
|
||||
arrayOf("alt", alt),
|
||||
) +
|
||||
options.map {
|
||||
val relayUrl = it.address.relay
|
||||
if (relayUrl != null) {
|
||||
arrayOf("option", it.option, it.address.toTag(), relayUrl)
|
||||
} else {
|
||||
arrayOf("option", it.option, it.address.toTag())
|
||||
}
|
||||
}
|
||||
).toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
class StoryOption(
|
||||
val option: String,
|
||||
val address: ATag,
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class InteractiveStoryPrologueEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : InteractiveStoryBaseEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
companion object {
|
||||
const val KIND = 30296
|
||||
const val ALT = "The prologue of an interative story called "
|
||||
|
||||
fun createAddressATag(
|
||||
pubKey: HexKey,
|
||||
dtag: String,
|
||||
): ATag = ATag(KIND, pubKey, dtag, null)
|
||||
|
||||
fun createAddressTag(
|
||||
pubKey: HexKey,
|
||||
dtag: String,
|
||||
): String = ATag.assembleATag(KIND, pubKey, dtag)
|
||||
|
||||
fun create(
|
||||
baseId: String,
|
||||
title: String,
|
||||
content: String,
|
||||
options: List<StoryOption>,
|
||||
summary: String? = null,
|
||||
image: String? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (InteractiveStoryPrologueEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
makeTags(baseId, ALT + title, title, summary, image, options) +
|
||||
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, imetas)
|
||||
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||
|
||||
@Immutable
|
||||
class InteractiveStoryReadingStateEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun title() = firstTag("title")
|
||||
|
||||
fun summary() = firstTag("summary")
|
||||
|
||||
fun image() = firstTag("image")
|
||||
|
||||
fun status() = firstTag("status")
|
||||
|
||||
fun root() =
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "A" }?.let {
|
||||
ATag.parse(it[1], it.getOrNull(2))
|
||||
}
|
||||
|
||||
fun currentScene() =
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "a" }?.let {
|
||||
ATag.parse(it[1], it.getOrNull(2))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 30298
|
||||
const val ALT1 = "Interactive Story Reading state"
|
||||
const val ALT2 = "The reading state of "
|
||||
|
||||
fun createAddressATag(
|
||||
pubKey: HexKey,
|
||||
dtag: String,
|
||||
): ATag = ATag(KIND, pubKey, dtag, null)
|
||||
|
||||
fun createAddressTag(
|
||||
pubKey: HexKey,
|
||||
dtag: String,
|
||||
): String = ATag.assembleATag(KIND, pubKey, dtag)
|
||||
|
||||
fun update(
|
||||
base: InteractiveStoryReadingStateEvent,
|
||||
currentScene: InteractiveStoryBaseEvent,
|
||||
currentSceneRelay: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
|
||||
) {
|
||||
val rootTag = base.dTag()
|
||||
val sceneTag = currentScene.addressTag()
|
||||
|
||||
val status =
|
||||
if (rootTag == sceneTag) {
|
||||
"new"
|
||||
} else if (currentScene.options().isEmpty()) {
|
||||
"done"
|
||||
} else {
|
||||
"reading"
|
||||
}
|
||||
|
||||
val tags =
|
||||
base.tags.filter { it[0] != "a" && it[0] != "status" } +
|
||||
listOf(
|
||||
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
|
||||
arrayOf("status", status),
|
||||
)
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
root: InteractiveStoryBaseEvent,
|
||||
rootRelay: String?,
|
||||
currentScene: InteractiveStoryBaseEvent,
|
||||
currentSceneRelay: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
|
||||
) {
|
||||
val rootTag = root.addressTag()
|
||||
val sceneTag = currentScene.addressTag()
|
||||
val status =
|
||||
if (rootTag == sceneTag) {
|
||||
"new"
|
||||
} else if (currentScene.options().isEmpty()) {
|
||||
"done"
|
||||
} else {
|
||||
"reading"
|
||||
}
|
||||
|
||||
val tags =
|
||||
listOfNotNull(
|
||||
arrayOf("d", rootTag),
|
||||
arrayOf("alt", root.title()?.let { ALT2 + it } ?: ALT1),
|
||||
root.title()?.let { arrayOf("title", it) },
|
||||
root.summary()?.let { arrayOf("summary", it) },
|
||||
root.image()?.let { arrayOf("image", it) },
|
||||
removeTrailingNullsAndEmptyOthers("A", rootTag, rootRelay),
|
||||
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
|
||||
arrayOf("status", status),
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ReadingStatus {
|
||||
NEW,
|
||||
READING,
|
||||
DONE,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class InteractiveStorySceneEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : InteractiveStoryBaseEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
companion object {
|
||||
const val KIND = 30297
|
||||
const val ALT = "A scene of an interative story called "
|
||||
|
||||
fun createAddressATag(
|
||||
pubKey: HexKey,
|
||||
dtag: String,
|
||||
): ATag = ATag(KIND, pubKey, dtag, null)
|
||||
|
||||
fun createAddressTag(
|
||||
pubKey: HexKey,
|
||||
dtag: String,
|
||||
): String = ATag.assembleATag(KIND, pubKey, dtag)
|
||||
|
||||
fun create(
|
||||
baseId: String,
|
||||
title: String,
|
||||
content: String,
|
||||
options: List<StoryOption>,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (InteractiveStorySceneEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
makeTags(baseId, ALT + title, title, options = options) +
|
||||
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, imetas)
|
||||
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -54,7 +55,9 @@ class LiveActivitiesChatMessageEvent(
|
||||
}
|
||||
}
|
||||
|
||||
override fun replyTos() = taggedEvents().minus(activityHex() ?: "")
|
||||
override fun markedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "")
|
||||
|
||||
override fun unMarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "")
|
||||
|
||||
companion object {
|
||||
const val KIND = 1311
|
||||
@@ -71,7 +74,7 @@ class LiveActivitiesChatMessageEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (LiveActivitiesChatMessageEvent) -> Unit,
|
||||
) {
|
||||
@@ -90,12 +93,8 @@ class LiveActivitiesChatMessageEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class LongTextNoteEvent(
|
||||
AddressableEvent {
|
||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
|
||||
|
||||
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ class MetadataEvent(
|
||||
nip05: String?,
|
||||
lnAddress: String?,
|
||||
lnURL: String?,
|
||||
pronouns: String?,
|
||||
twitter: String?,
|
||||
mastodon: String?,
|
||||
github: String?,
|
||||
@@ -231,6 +232,7 @@ class MetadataEvent(
|
||||
picture?.let { addIfNotBlank(currentJson, "picture", it.trim()) }
|
||||
banner?.let { addIfNotBlank(currentJson, "banner", it.trim()) }
|
||||
website?.let { addIfNotBlank(currentJson, "website", it.trim()) }
|
||||
pronouns?.let { addIfNotBlank(currentJson, "pronouns", it.trim()) }
|
||||
about?.let { addIfNotBlank(currentJson, "about", it.trim()) }
|
||||
nip05?.let { addIfNotBlank(currentJson, "nip05", it.trim()) }
|
||||
lnAddress?.let { addIfNotBlank(currentJson, "lud16", it.trim()) }
|
||||
@@ -281,7 +283,7 @@ class MetadataEvent(
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
if (value.isBlank()) {
|
||||
if (value.isBlank() || value == "null") {
|
||||
currentJson.remove(key)
|
||||
} else {
|
||||
currentJson.put(key, value.trim())
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
class NIP17Factory {
|
||||
@@ -79,7 +81,7 @@ class NIP17Factory {
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
draftTag: String? = null,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
@@ -97,7 +99,66 @@ class NIP17Factory {
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
isDraft = draftTag != null,
|
||||
nip94attachments = nip94attachments,
|
||||
imetas = imetas,
|
||||
) { senderMessage ->
|
||||
if (draftTag != null) {
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = listOf(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createEncryptedFileNIP17(
|
||||
url: String,
|
||||
to: List<HexKey>,
|
||||
repliesToHex: List<HexKey>? = null,
|
||||
contentType: String?,
|
||||
algo: String,
|
||||
key: ByteArray,
|
||||
nonce: ByteArray? = null,
|
||||
originalHash: String? = null,
|
||||
hash: String? = null,
|
||||
size: Int? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
alt: String?,
|
||||
draftTag: String? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
val senderPublicKey = signer.pubKey
|
||||
|
||||
ChatMessageEncryptedFileHeaderEvent.create(
|
||||
url = url,
|
||||
to = to,
|
||||
repliesTo = repliesToHex,
|
||||
contentType = contentType,
|
||||
algo = algo,
|
||||
key = key,
|
||||
nonce = nonce,
|
||||
originalHash = originalHash,
|
||||
hash = hash,
|
||||
size = size,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
sensitiveContent = sensitiveContent,
|
||||
alt = alt,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) { senderMessage ->
|
||||
if (draftTag != null) {
|
||||
onReady(
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.ETag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments.Companion.IMETA
|
||||
import com.vitorpamplona.quartz.encoders.PTag
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class PictureEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
fun mimeTypes() = tags.filter { it.size > 1 && it[0] == MIME_TYPE }
|
||||
|
||||
fun hashes() = tags.filter { it.size > 1 && it[0] == HASH }
|
||||
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == TITLE }?.get(1)
|
||||
|
||||
private fun url() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.URL }?.get(1)
|
||||
|
||||
private fun urls() = tags.filter { it.size > 1 && it[0] == PictureMeta.URL }.map { it[1] }
|
||||
|
||||
private fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.MIME_TYPE }?.get(1)
|
||||
|
||||
private fun hash() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.HASH }?.get(1)
|
||||
|
||||
private fun size() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.FILE_SIZE }?.get(1)
|
||||
|
||||
private fun alt() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.ALT }?.get(1)
|
||||
|
||||
private fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.DIMENSION }?.get(1)?.let { Dimension.parse(it) }
|
||||
|
||||
private fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == PictureMeta.BLUR_HASH }?.get(1)
|
||||
|
||||
private fun hasUrl() = tags.any { it.size > 1 && it[0] == PictureMeta.URL }
|
||||
|
||||
// hack to fix pablo's bug
|
||||
fun rootImage() =
|
||||
url()?.let {
|
||||
PictureMeta(
|
||||
url = it,
|
||||
mimeType = mimeType(),
|
||||
blurhash = blurhash(),
|
||||
alt = alt(),
|
||||
hash = hash(),
|
||||
dimension = dimensions(),
|
||||
size = size()?.toLongOrNull(),
|
||||
fallback = emptyList(),
|
||||
annotations = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
fun imetaTags() =
|
||||
tags
|
||||
.map { tagArray ->
|
||||
if (tagArray.size > 1 && tagArray[0] == IMETA) {
|
||||
PictureMeta.parse(tagArray)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.plus(rootImage())
|
||||
.filterNotNull()
|
||||
|
||||
companion object {
|
||||
const val KIND = 20
|
||||
const val ALT_DESCRIPTION = "List of pictures"
|
||||
|
||||
private const val MIME_TYPE = "m"
|
||||
private const val HASH = "x"
|
||||
private const val TITLE = "title"
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
msg: String? = null,
|
||||
title: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: Long? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
usersMentioned: Set<PTag> = emptySet(),
|
||||
addressesMentioned: Set<ATag> = emptySet(),
|
||||
eventsMentioned: Set<ETag> = emptySet(),
|
||||
geohash: String? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PictureEvent) -> Unit,
|
||||
) {
|
||||
val image =
|
||||
PictureMeta(
|
||||
url,
|
||||
mimeType,
|
||||
blurhash,
|
||||
dimensions,
|
||||
alt,
|
||||
hash,
|
||||
size,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
create(listOf(image), msg, title, usersMentioned, addressesMentioned, eventsMentioned, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
images: List<PictureMeta>,
|
||||
msg: String? = null,
|
||||
title: String? = null,
|
||||
usersMentioned: Set<PTag> = emptySet(),
|
||||
addressesMentioned: Set<ATag> = emptySet(),
|
||||
eventsMentioned: Set<ETag> = emptySet(),
|
||||
geohash: String? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PictureEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf(arrayOf<String>("alt", ALT_DESCRIPTION))
|
||||
|
||||
images.forEach {
|
||||
tags.add(it.toIMetaArray())
|
||||
}
|
||||
|
||||
title?.let { tags.add(arrayOf("title", it)) }
|
||||
|
||||
images.distinctBy { it.hash }.forEach {
|
||||
if (it.hash != null) {
|
||||
tags.add(arrayOf("x", it.hash))
|
||||
}
|
||||
}
|
||||
|
||||
images.distinctBy { it.mimeType }.forEach {
|
||||
if (it.mimeType != null) {
|
||||
tags.add(arrayOf("m", it.mimeType))
|
||||
}
|
||||
}
|
||||
|
||||
usersMentioned.forEach { tags.add(it.toPTagArray()) }
|
||||
addressesMentioned.forEach { tags.add(it.toQTagArray()) }
|
||||
eventsMentioned.forEach { tags.add(it.toQTagArray()) }
|
||||
|
||||
if (msg != null) {
|
||||
findHashtags(msg).forEach {
|
||||
val lowercaseTag = it.lowercase()
|
||||
tags.add(arrayOf("t", it))
|
||||
if (it != lowercaseTag) {
|
||||
tags.add(arrayOf("t", it.lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
findURLs(msg).forEach { tags.add(arrayOf("r", it)) }
|
||||
}
|
||||
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(arrayOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg ?: "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PictureMeta(
|
||||
val url: String,
|
||||
val mimeType: String?,
|
||||
val blurhash: String?,
|
||||
val dimension: Dimension?,
|
||||
val alt: String?,
|
||||
val hash: String?,
|
||||
val size: Long?,
|
||||
val fallback: List<String>,
|
||||
val annotations: List<UserAnnotation>,
|
||||
) {
|
||||
fun toIMetaArray(): Array<String> =
|
||||
(
|
||||
listOfNotNull(
|
||||
"imeta",
|
||||
"$URL $url",
|
||||
mimeType?.let { "$MIME_TYPE $it" },
|
||||
alt?.let { "$ALT $it" },
|
||||
hash?.let { "$HASH $it" },
|
||||
size?.let { "$FILE_SIZE $it" },
|
||||
dimension?.let { "$DIMENSION $it" },
|
||||
blurhash?.let { "$BLUR_HASH $it" },
|
||||
) +
|
||||
fallback.map { "$FALLBACK $it" } +
|
||||
annotations.map { "$ANNOTATIONS $it" }
|
||||
).toTypedArray()
|
||||
|
||||
companion object {
|
||||
const val URL = "url"
|
||||
const val MIME_TYPE = "m"
|
||||
const val FILE_SIZE = "size"
|
||||
const val DIMENSION = "dim"
|
||||
const val HASH = "x"
|
||||
const val BLUR_HASH = "blurhash"
|
||||
const val ALT = "alt"
|
||||
const val FALLBACK = "fallback"
|
||||
const val ANNOTATIONS = "annotate-user"
|
||||
|
||||
fun parse(tagArray: Array<String>): PictureMeta? {
|
||||
var url: String? = null
|
||||
var mimeType: String? = null
|
||||
var blurhash: String? = null
|
||||
var dim: Dimension? = null
|
||||
var alt: String? = null
|
||||
var hash: String? = null
|
||||
var size: Long? = null
|
||||
val fallback = mutableListOf<String>()
|
||||
val annotations = mutableListOf<UserAnnotation>()
|
||||
|
||||
if (tagArray.size == 2 && tagArray[1].contains(URL) && (tagArray[1].contains(BLUR_HASH) || tagArray[1].contains(FILE_SIZE))) {
|
||||
// hack to fix pablo's bug
|
||||
val keys = setOf(URL, MIME_TYPE, BLUR_HASH, DIMENSION, ALT, HASH, FILE_SIZE, FALLBACK, ANNOTATIONS)
|
||||
var keyNextValue: String? = null
|
||||
val values = mutableListOf<String>()
|
||||
|
||||
tagArray[1].split(" ").forEach {
|
||||
if (it in keys) {
|
||||
if (keyNextValue != null && values.isNotEmpty()) {
|
||||
when (keyNextValue) {
|
||||
URL -> url = values.joinToString(" ")
|
||||
MIME_TYPE -> mimeType = values.joinToString(" ")
|
||||
BLUR_HASH -> blurhash = values.joinToString(" ")
|
||||
DIMENSION -> dim = Dimension.parse(values.joinToString(" "))
|
||||
ALT -> alt = values.joinToString(" ")
|
||||
HASH -> hash = values.joinToString(" ")
|
||||
FILE_SIZE -> size = values.joinToString(" ").toLongOrNull()
|
||||
FALLBACK -> fallback.add(values.joinToString(" "))
|
||||
ANNOTATIONS -> {
|
||||
UserAnnotation.parse(values.joinToString(" "))?.let {
|
||||
annotations.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
values.clear()
|
||||
}
|
||||
keyNextValue = it
|
||||
} else {
|
||||
values.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (keyNextValue != null && values.isNotEmpty()) {
|
||||
when (keyNextValue) {
|
||||
URL -> url = values.joinToString(" ")
|
||||
MIME_TYPE -> mimeType = values.joinToString(" ")
|
||||
BLUR_HASH -> blurhash = values.joinToString(" ")
|
||||
DIMENSION -> dim = Dimension.parse(values.joinToString(" "))
|
||||
ALT -> alt = values.joinToString(" ")
|
||||
HASH -> hash = values.joinToString(" ")
|
||||
FILE_SIZE -> size = values.joinToString(" ").toLongOrNull()
|
||||
FALLBACK -> fallback.add(values.joinToString(" "))
|
||||
ANNOTATIONS -> {
|
||||
UserAnnotation.parse(values.joinToString(" "))?.let {
|
||||
annotations.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
values.clear()
|
||||
keyNextValue = null
|
||||
}
|
||||
} else {
|
||||
tagArray.forEach {
|
||||
val parts = it.split(" ", limit = 2)
|
||||
val key = parts[0]
|
||||
val value = if (parts.size == 2) parts[1] else ""
|
||||
|
||||
if (value.isNotBlank()) {
|
||||
when (key) {
|
||||
URL -> url = value
|
||||
MIME_TYPE -> mimeType = value
|
||||
BLUR_HASH -> blurhash = value
|
||||
DIMENSION -> dim = Dimension.parse(value)
|
||||
ALT -> alt = value
|
||||
HASH -> hash = value
|
||||
FILE_SIZE -> size = value.toLongOrNull()
|
||||
FALLBACK -> fallback.add(value)
|
||||
ANNOTATIONS -> {
|
||||
UserAnnotation.parse(value)?.let {
|
||||
annotations.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url?.let {
|
||||
PictureMeta(it, mimeType, blurhash, dim, alt, hash, size, fallback, annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UserAnnotation(
|
||||
val pubkey: HexKey,
|
||||
val x: Int,
|
||||
val y: Int,
|
||||
) {
|
||||
override fun toString() = "$pubkey:$x:$y"
|
||||
|
||||
companion object {
|
||||
fun parse(value: String): UserAnnotation? {
|
||||
val ann = value.split(":")
|
||||
if (ann.size == 3) {
|
||||
val x = ann[1].toIntOrNull()
|
||||
val y = ann[2].toIntOrNull()
|
||||
if (x != null && y != null) {
|
||||
return UserAnnotation(ann[0], x, y)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -79,7 +80,7 @@ class PollNoteEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (PollNoteEvent) -> Unit,
|
||||
) {
|
||||
@@ -104,12 +105,8 @@ class PollNoteEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip54InlineMetadata
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -124,16 +125,13 @@ class PrivateDmEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (PrivateDmEvent) -> Unit,
|
||||
) {
|
||||
var message = msg
|
||||
nip94attachments?.forEach {
|
||||
val myUrl = it.url()
|
||||
if (myUrl != null) {
|
||||
message = message.replace(myUrl, Nip54InlineMetadata().createUrl(myUrl, it.tags))
|
||||
}
|
||||
imetas?.forEach {
|
||||
message = message.replace(it.url, Nip54InlineMetadata().createUrl(it.url, it.properties))
|
||||
}
|
||||
|
||||
message =
|
||||
@@ -156,13 +154,10 @@ class PrivateDmEvent(
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
/* Privacy issue: DO NOT ADD THESE TO THE TAGS.
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
*/
|
||||
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||
import com.vitorpamplona.quartz.utils.remove
|
||||
import com.vitorpamplona.quartz.utils.replaceAll
|
||||
|
||||
@Immutable
|
||||
abstract class PrivateTagArrayEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun cachedPrivateTags(): Array<Array<String>>? = privateTagsCache
|
||||
|
||||
fun privateTags(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) {
|
||||
onReady(emptyArray())
|
||||
return
|
||||
}
|
||||
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
signer.decrypt(content, pubKey) {
|
||||
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
|
||||
privateTagsCache?.let { onReady(it) }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptChangeEncrypt(
|
||||
signer: NostrSigner,
|
||||
change: (Array<Array<String>>) -> Array<Array<String>>,
|
||||
onReady: (content: String) -> Unit,
|
||||
) {
|
||||
privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = change(privateTags),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun add(
|
||||
current: PrivateTagArrayEvent,
|
||||
newTag: Array<String>,
|
||||
toPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (toPrivate) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.plus(newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onReady(current.content, current.tags.plus(newTag))
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(
|
||||
current: PrivateTagArrayEvent,
|
||||
newTag: Array<Array<String>>,
|
||||
toPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (toPrivate) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.plus(newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onReady(current.content, current.tags.plus(newTag))
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPrivateNewTag(
|
||||
dTag: String,
|
||||
current: PrivateTagArrayEvent?,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (current == null) {
|
||||
createPrivate(dTag, newTag, signer, onReady)
|
||||
} else {
|
||||
replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPublicNewTag(
|
||||
dTag: String,
|
||||
current: PrivateTagArrayEvent?,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (current == null) {
|
||||
createPublic(dTag, newTag, signer, onReady)
|
||||
} else {
|
||||
replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPrivateNewTag(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.replaceAll(oldTagStartsWith, newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllToPublicNewTag(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.remove(oldTagStartsWith),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllFromPrivate(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.remove(oldTagStartsWith),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllFromPublic(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) = onReady(current.content, current.tags.remove(oldTagStartsWith))
|
||||
|
||||
fun removeAll(
|
||||
current: PrivateTagArrayEvent,
|
||||
oldTagStartsWith: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
current.privateTags(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.remove(oldTagStartsWith),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createPrivate(
|
||||
dTag: String,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
encryptTags(
|
||||
privateTags = arrayOf(newTag),
|
||||
signer = signer,
|
||||
) { encryptedTags ->
|
||||
onReady(encryptedTags, arrayOf(arrayOf("d", dTag)))
|
||||
}
|
||||
}
|
||||
|
||||
fun createPublic(
|
||||
dTag: String,
|
||||
newTag: Array<String>,
|
||||
signer: NostrSigner,
|
||||
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
onReady("", arrayOf(arrayOf("d", dTag), newTag))
|
||||
}
|
||||
|
||||
fun encryptTags(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) = signer.nip04Encrypt(
|
||||
if (privateTags.isNullOrEmpty()) "" else mapper.writeValueAsString(privateTags),
|
||||
signer.pubKey,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -38,8 +39,6 @@ class ProfileGalleryEntryEvent(
|
||||
|
||||
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[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)
|
||||
@@ -48,7 +47,7 @@ class ProfileGalleryEntryEvent(
|
||||
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
|
||||
|
||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||
|
||||
@@ -89,12 +88,11 @@ class ProfileGalleryEntryEvent(
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
@@ -112,12 +110,11 @@ class ProfileGalleryEntryEvent(
|
||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it.toString()) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class RelationshipStatusEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
companion object {
|
||||
const val KIND = 30382
|
||||
const val ALT = "Relationship Status"
|
||||
|
||||
const val PETNAME = "petname"
|
||||
const val SUMMARY = "summary"
|
||||
|
||||
private fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (RelationshipStatusEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + arrayOf("alt", ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
targetUser: HexKey,
|
||||
petname: String? = null,
|
||||
summary: String? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (RelationshipStatusEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
tags.add(arrayOf("d", targetUser))
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
val privateTags = mutableListOf<Array<String>>()
|
||||
petname?.let { privateTags.add(arrayOf(PETNAME, it)) }
|
||||
summary?.let { privateTags.add(arrayOf(SUMMARY, it)) }
|
||||
|
||||
encryptTags(privateTags.toTypedArray(), signer) { content ->
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -56,7 +57,7 @@ class TextNoteEvent(
|
||||
root: String? = null,
|
||||
directMentions: Set<HexKey> = emptySet(),
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
forkedFrom: Event? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
@@ -122,12 +123,8 @@ class TextNoteEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
|
||||
if (isDraft) {
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.IMetaTag
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -61,7 +62,7 @@ class TorrentCommentEvent(
|
||||
directMentions: Set<HexKey> = emptySet(),
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
forkedFrom: Event? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (TorrentCommentEvent) -> Unit,
|
||||
@@ -122,12 +123,8 @@ class TorrentCommentEvent(
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
imetas?.forEach {
|
||||
tags.add(Nip92MediaAttachments.createTag(it))
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments.Companion.IMETA
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@@ -34,53 +36,75 @@ abstract class VideoEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig),
|
||||
RootScope {
|
||||
private fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
|
||||
|
||||
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
|
||||
private fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
|
||||
|
||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||
private 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)
|
||||
private 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)
|
||||
private fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
||||
|
||||
private fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
|
||||
|
||||
private fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
||||
|
||||
private fun image() = tags.filter { it.size > 1 && it[0] == IMAGE }.map { it[1] }
|
||||
|
||||
private fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == THUMB }?.get(1)
|
||||
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
|
||||
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 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 thumb() = tags.firstOrNull { it.size > 1 && it[0] == THUMB }?.get(1)
|
||||
fun duration() = tags.firstOrNull { it.size > 1 && it[0] == DURATION }?.get(1)
|
||||
|
||||
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
|
||||
|
||||
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE && mimeTypes.contains(it[1]) }
|
||||
|
||||
// hack to fix pablo's bug
|
||||
fun rootVideo() =
|
||||
url()?.let {
|
||||
VideoMeta(
|
||||
url = it,
|
||||
mimeType = mimeType(),
|
||||
blurhash = blurhash(),
|
||||
alt = alt(),
|
||||
hash = hash(),
|
||||
dimension = dimensions(),
|
||||
size = size()?.toIntOrNull(),
|
||||
service = null,
|
||||
fallback = emptyList(),
|
||||
image = image(),
|
||||
)
|
||||
}
|
||||
|
||||
fun imetaTags() =
|
||||
tags
|
||||
.map { tagArray ->
|
||||
if (tagArray.size > 1 && tagArray[0] == IMETA) {
|
||||
VideoMeta.parse(tagArray)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.plus(rootVideo())
|
||||
.filterNotNull()
|
||||
|
||||
companion object {
|
||||
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"
|
||||
private const val ORIGINAL_HASH = "ox"
|
||||
private const val ALT = "alt"
|
||||
private const val TITLE = "title"
|
||||
private const val PUBLISHED_AT = "published_at"
|
||||
private const val SUMMARY = "summary"
|
||||
private const val DURATION = "duration"
|
||||
private const val IMAGE = "image"
|
||||
@@ -88,49 +112,131 @@ abstract class VideoEvent(
|
||||
|
||||
fun <T : VideoEvent> create(
|
||||
kind: Int,
|
||||
dTag: String,
|
||||
url: String,
|
||||
magnetUri: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
size: Int? = null,
|
||||
duration: Int? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
service: String? = null,
|
||||
altDescription: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
listOfNotNull(
|
||||
arrayOf(URL, url),
|
||||
magnetUri?.let { arrayOf(MAGNET_URI, it) },
|
||||
mimeType?.let { arrayOf(MIME_TYPE, it) },
|
||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", altDescription),
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
val video =
|
||||
VideoMeta(
|
||||
url,
|
||||
mimeType,
|
||||
blurhash,
|
||||
dimensions,
|
||||
alt,
|
||||
hash,
|
||||
size,
|
||||
service,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
tags.add(arrayOf("d", dTag))
|
||||
tags.add(arrayOf(ALT, altDescription))
|
||||
if (sensitiveContent == true) {
|
||||
tags.add(arrayOf("content-warning", ""))
|
||||
}
|
||||
duration?.let { tags.add(arrayOf(DURATION, "duration")) }
|
||||
|
||||
tags.add(video.toIMetaArray())
|
||||
|
||||
val content = alt ?: ""
|
||||
signer.sign<T>(createdAt, kind, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class VideoMeta(
|
||||
val url: String,
|
||||
val mimeType: String?,
|
||||
val blurhash: String?,
|
||||
val dimension: Dimension?,
|
||||
val alt: String?,
|
||||
val hash: String?,
|
||||
val size: Int?,
|
||||
val service: String?,
|
||||
val fallback: List<String>,
|
||||
val image: List<String>,
|
||||
) {
|
||||
fun toIMetaArray(): Array<String> =
|
||||
(
|
||||
listOfNotNull(
|
||||
"imeta",
|
||||
"$URL $url",
|
||||
mimeType?.let { "$MIME_TYPE $it" },
|
||||
alt?.let { "$ALT $it" },
|
||||
hash?.let { "$HASH $it" },
|
||||
size?.let { "$FILE_SIZE $it" },
|
||||
dimension?.let { "$DIMENSION $it" },
|
||||
blurhash?.let { "$BLUR_HASH $it" },
|
||||
service?.let { "$SERVICE $it" },
|
||||
) +
|
||||
fallback.map { "$FALLBACK $it" } +
|
||||
image.map { "$IMAGE $it" }
|
||||
|
||||
).toTypedArray()
|
||||
|
||||
companion object {
|
||||
const val URL = "url"
|
||||
const val MIME_TYPE = "m"
|
||||
const val FILE_SIZE = "size"
|
||||
const val DIMENSION = "dim"
|
||||
const val HASH = "x"
|
||||
const val BLUR_HASH = "blurhash"
|
||||
const val ALT = "alt"
|
||||
const val FALLBACK = "fallback"
|
||||
const val IMAGE = "image"
|
||||
const val SERVICE = "service"
|
||||
|
||||
fun parse(tagArray: Array<String>): VideoMeta? {
|
||||
var url: String? = null
|
||||
var mimeType: String? = null
|
||||
var blurhash: String? = null
|
||||
var dim: Dimension? = null
|
||||
var alt: String? = null
|
||||
var hash: String? = null
|
||||
var size: Int? = null
|
||||
var service: String? = null
|
||||
val fallback = mutableListOf<String>()
|
||||
val images = mutableListOf<String>()
|
||||
|
||||
tagArray.forEach {
|
||||
val parts = it.split(" ", limit = 2)
|
||||
val key = parts[0]
|
||||
val value = if (parts.size == 2) parts[1] else ""
|
||||
|
||||
if (value.isNotBlank()) {
|
||||
when (key) {
|
||||
URL -> url = value
|
||||
MIME_TYPE -> mimeType = value
|
||||
BLUR_HASH -> blurhash = value
|
||||
DIMENSION -> dim = Dimension.parse(value)
|
||||
ALT -> alt = value
|
||||
HASH -> hash = value
|
||||
FILE_SIZE -> size = value.toIntOrNull()
|
||||
SERVICE -> service = value
|
||||
FALLBACK -> fallback.add(value)
|
||||
IMAGE -> images.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url?.let {
|
||||
VideoMeta(it, mimeType, blurhash, dim, alt, hash, size, service, fallback, images)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.util.UUID
|
||||
|
||||
@Immutable
|
||||
class VideoHorizontalEvent(
|
||||
@@ -33,48 +35,45 @@ class VideoHorizontalEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : VideoEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : VideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
companion object {
|
||||
const val KIND = 34235
|
||||
const val ALT_DESCRIPTION = "Horizontal Video"
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
magnetUri: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
size: Int? = null,
|
||||
duration: Int? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
service: String? = null,
|
||||
dTag: String = UUID.randomUUID().toString(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (VideoHorizontalEvent) -> Unit,
|
||||
) {
|
||||
create(
|
||||
KIND,
|
||||
url,
|
||||
magnetUri,
|
||||
mimeType,
|
||||
alt,
|
||||
hash,
|
||||
size,
|
||||
dimensions,
|
||||
blurhash,
|
||||
originalHash,
|
||||
magnetURI,
|
||||
torrentInfoHash,
|
||||
encryptionKey,
|
||||
sensitiveContent,
|
||||
ALT_DESCRIPTION,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
kind = KIND,
|
||||
dTag = dTag,
|
||||
url = url,
|
||||
mimeType = mimeType,
|
||||
alt = alt,
|
||||
hash = hash,
|
||||
size = size,
|
||||
duration = duration,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
sensitiveContent = sensitiveContent,
|
||||
service = service,
|
||||
altDescription = ALT_DESCRIPTION,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.util.UUID
|
||||
|
||||
@Immutable
|
||||
class VideoVerticalEvent(
|
||||
@@ -33,48 +35,45 @@ class VideoVerticalEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : VideoEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : VideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
companion object {
|
||||
const val KIND = 34236
|
||||
const val ALT_DESCRIPTION = "Vertical Video"
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
magnetUri: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
size: Int? = null,
|
||||
duration: Int? = null,
|
||||
dimensions: Dimension? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
service: String? = null,
|
||||
dTag: String = UUID.randomUUID().toString(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (VideoVerticalEvent) -> Unit,
|
||||
) {
|
||||
create(
|
||||
KIND,
|
||||
url,
|
||||
magnetUri,
|
||||
mimeType,
|
||||
alt,
|
||||
hash,
|
||||
size,
|
||||
dimensions,
|
||||
blurhash,
|
||||
originalHash,
|
||||
magnetURI,
|
||||
torrentInfoHash,
|
||||
encryptionKey,
|
||||
sensitiveContent,
|
||||
ALT_DESCRIPTION,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
kind = KIND,
|
||||
dTag = dTag,
|
||||
url = url,
|
||||
mimeType = mimeType,
|
||||
alt = alt,
|
||||
hash = hash,
|
||||
size = size,
|
||||
duration = duration,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
sensitiveContent = sensitiveContent,
|
||||
service = service,
|
||||
altDescription = ALT_DESCRIPTION,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class WikiNoteEvent(
|
||||
AddressableEvent {
|
||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
|
||||
|
||||
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
public fun removeTrailingNullsAndEmptyOthers(vararg elements: String?): Array<String> {
|
||||
val lastNonNullIndex = elements.indexOfLast { it != null }
|
||||
|
||||
if (lastNonNullIndex < 0) return Array(0) { "" }
|
||||
|
||||
return Array(lastNonNullIndex + 1) { index ->
|
||||
elements[index] ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
fun Array<String>.startsWith(startsWith: Array<String>): Boolean {
|
||||
if (startsWith.size > this.size) return false
|
||||
for (tagIdx in startsWith.indices) {
|
||||
if (startsWith[tagIdx] != this[tagIdx]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inline fun Array<Array<String>>.filterToArray(predicate: (Array<String>) -> Boolean): Array<Array<String>> = filterTo(ArrayList(), predicate).toTypedArray()
|
||||
|
||||
inline fun Array<Array<String>>.remove(predicate: (Array<String>) -> Boolean): Array<Array<String>> = filterNotTo(ArrayList(this.size), predicate).toTypedArray()
|
||||
|
||||
inline fun Array<Array<String>>.remove(startsWith: Array<String>): Array<Array<String>> = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray()
|
||||
|
||||
inline fun Array<Array<String>>.replaceAll(
|
||||
startsWith: Array<String>,
|
||||
newElement: Array<String>,
|
||||
): Array<Array<String>> = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).plusElement(newElement).toTypedArray()
|
||||
Reference in New Issue
Block a user