amethyst/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt

544 wiersze
19 KiB
Kotlin

/**
* 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.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
@Immutable
open class Event(
val id: HexKey,
@JsonProperty("pubkey") val pubKey: HexKey,
@JsonProperty("created_at") val createdAt: Long,
val kind: Int,
val tags: Array<Array<String>>,
val content: String,
val sig: HexKey,
) : EventInterface {
override fun isContentEncoded() = false
override fun countMemory(): Long {
return 12L +
id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() +
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
content.bytesUsedInMemory() +
sig.bytesUsedInMemory()
}
override fun id(): HexKey = id
override fun pubKey(): HexKey = pubKey
override fun createdAt(): Long = createdAt
override fun kind(): Int = kind
override fun tags(): Array<Array<String>> = tags
override fun content(): String = content
override fun sig(): HexKey = sig
override fun toJson(): String = mapper.writeValueAsString(toJsonObject())
override fun hasAnyTaggedUser() = hasTagWithContent("p")
override fun hasTagWithContent(tagName: String) = tags.any { it.size > 1 && it[0] == tagName }
override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] }
override fun firstTaggedEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
override fun firstTaggedK() = tags.firstOrNull { it.size > 1 && it[0] == "k" }?.let { it[1].toIntOrNull() }
override fun firstTaggedAddress() =
tags
.firstOrNull { it.size > 1 && it[0] == "a" }
?.let {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
override fun isSensitive() =
tags.any {
(it.size > 0 && it[0].equals("content-warning", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nude", true))
}
override fun subject() = tags.firstOrNull { it.size > 1 && it[0] == "subject" }?.get(1)
override fun zapraiserAmount() = tags.firstOrNull { (it.size > 1 && it[0] == "zapraiser") }?.get(1)?.toLongOrNull()
override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" }
override fun zapSplitSetup(): List<ZapSplitSetup> {
return tags
.filter { it.size > 1 && it[0] == "zap" }
.mapNotNull {
val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true)
val weight = if (isLnAddress) 1.0 else (it.getOrNull(3)?.toDoubleOrNull() ?: 0.0)
if (weight > 0) {
ZapSplitSetup(
it[1],
it.getOrNull(2),
weight,
isLnAddress,
)
} else {
null
}
}
}
override fun taggedAddresses() =
tags
.filter { it.size > 1 && it[0] == "a" }
.mapNotNull {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
override fun hasHashtags() = tags.any { it.size > 1 && it[0] == "t" }
override fun hasGeohashes() = tags.any { it.size > 1 && it[0] == "g" }
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
override fun geohashes() = tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] }
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
override fun isTagged(
key: String,
tag: String,
) = tags.any { it.size > 1 && it[0] == key && it[1] == tag }
override fun isAnyTagged(
key: String,
tags: Set<String>,
) = this.tags.any { it.size > 1 && it[0] == key && it[1] in tags }
override fun isTaggedWord(word: String) = isTagged("word", word)
override fun isTaggedUser(idHex: String) = isTagged("p", idHex)
override fun isTaggedUsers(idHexes: Set<String>) = isAnyTagged("p", idHexes)
override fun isTaggedEvent(idHex: String) = isTagged("e", idHex)
override fun isTaggedAddressableNote(idHex: String) = isTagged("a", idHex)
override fun isTaggedAddressableNotes(idHexes: Set<String>) = isAnyTagged("a", idHexes)
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
override fun isTaggedGeoHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "g" && it[1].startsWith(hashtag, true) }
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
override fun isTaggedGeoHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "g" && it[1].lowercase() in hashtags }
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
override fun firstIsTaggedAddressableNote(addressableNotes: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1)
override fun isTaggedAddressableKind(kind: Int): Boolean {
val kindStr = kind.toString()
return tags.any { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
}
override fun expiration() =
try {
tags.firstOrNull { it.size > 1 && it[0] == "expiration" }?.get(1)?.toLongOrNull()
} catch (_: Exception) {
null
}
override fun isExpired() = (expiration() ?: Long.MAX_VALUE) < TimeUtils.now()
override fun isExpirationBefore(time: Long) = (expiration() ?: Long.MAX_VALUE) < time
override fun getTagOfAddressableKind(kind: Int): ATag? {
val kindStr = kind.toString()
val aTag =
tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }?.getOrNull(1)
?: return null
return ATag.parse(aTag, null)
}
override fun getPoWRank(): Int {
var rank = 0
for (i in 0..id.length) {
if (id[i] == '0') {
rank += 4
} else if (id[i] in '4'..'7') {
rank += 1
break
} else if (id[i] in '2'..'3') {
rank += 2
break
} else if (id[i] == '1') {
rank += 3
break
} else {
break
}
}
return rank
}
override fun getGeoHash(): String? {
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
}
override fun getReward(): BigDecimal? {
return try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
} catch (e: Exception) {
null
}
}
open fun toNIP19(): String {
return if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr()
} else {
Nip19Bech32.createNEvent(id, pubKey, kind, null)
}
}
fun toNostrUri(): String {
return "nostr:${toNIP19()}"
}
fun hasCorrectIDHash(): Boolean {
if (id.isEmpty()) return false
return id.equals(generateId())
}
fun hasVerifiedSignature(): Boolean {
if (id.isEmpty() || sig.isEmpty()) return false
return CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
}
/** Checks if the ID is correct and then if the pubKey's secret key signed the event. */
override fun checkSignature() {
if (!hasCorrectIDHash()) {
throw Exception(
"""
|Unexpected ID.
| Event: ${toJson()}
| Actual ID: $id
| Generated: ${generateId()}
"""
.trimIndent(),
)
}
if (!hasVerifiedSignature()) {
throw Exception("""Bad signature!""")
}
}
override fun hasValidSignature(): Boolean {
return try {
hasCorrectIDHash() && hasVerifiedSignature()
} catch (e: Exception) {
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
false
}
}
fun makeJsonForId(): String {
return makeJsonForId(pubKey, createdAt, kind, tags, content)
}
private fun generateId(): String {
return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
}
private class EventDeserializer : StdDeserializer<Event>(Event::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Event {
return fromJson(jp.codec.readTree(jp))
}
}
private class GossipDeserializer : StdDeserializer<Gossip>(Gossip::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Gossip {
val jsonObject: JsonNode = jp.codec.readTree(jp)
return Gossip(
id = jsonObject.get("id")?.asText()?.intern(),
pubKey = jsonObject.get("pubkey")?.asText()?.intern(),
createdAt = jsonObject.get("created_at")?.asLong(),
kind = jsonObject.get("kind")?.asInt(),
tags =
jsonObject.get("tags").toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
},
content = jsonObject.get("content")?.asText(),
)
}
}
private class EventSerializer : StdSerializer<Event>(Event::class.java) {
override fun serialize(
event: Event,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
gen.writeStringField("id", event.id)
gen.writeStringField("pubkey", event.pubKey)
gen.writeNumberField("created_at", event.createdAt)
gen.writeNumberField("kind", event.kind)
gen.writeArrayFieldStart("tags")
event.tags.forEach { tag -> gen.writeArray(tag, 0, tag.size) }
gen.writeEndArray()
gen.writeStringField("content", event.content)
gen.writeStringField("sig", event.sig)
gen.writeEndObject()
}
}
private class GossipSerializer : StdSerializer<Gossip>(Gossip::class.java) {
override fun serialize(
event: Gossip,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
event.id?.let { gen.writeStringField("id", it) }
event.pubKey?.let { gen.writeStringField("pubkey", it) }
event.createdAt?.let { gen.writeNumberField("created_at", it) }
event.kind?.let { gen.writeNumberField("kind", it) }
event.tags?.let {
gen.writeArrayFieldStart("tags")
event.tags.forEach { tag -> gen.writeArray(tag, 0, tag.size) }
gen.writeEndArray()
}
event.content?.let { gen.writeStringField("content", it) }
gen.writeEndObject()
}
}
fun toJsonObject(): JsonNode {
val factory = mapper.nodeFactory
return factory.objectNode().apply {
put("id", id)
put("pubkey", pubKey)
put("created_at", createdAt)
put("kind", kind)
replace(
"tags",
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
put("content", content)
put("sig", sig)
}
}
companion object {
val mapper =
jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
.registerModule(
SimpleModule()
.addSerializer(Event::class.java, EventSerializer())
.addDeserializer(Event::class.java, EventDeserializer())
.addSerializer(Gossip::class.java, GossipSerializer())
.addDeserializer(Gossip::class.java, GossipDeserializer())
.addDeserializer(Response::class.java, ResponseDeserializer())
.addDeserializer(Request::class.java, RequestDeserializer()),
)
fun fromJson(jsonObject: JsonNode): Event {
return EventFactory.create(
id = jsonObject.get("id").asText().intern(),
pubKey = jsonObject.get("pubkey").asText().intern(),
createdAt = jsonObject.get("created_at").asLong(),
kind = jsonObject.get("kind").asInt(),
tags =
jsonObject.get("tags").toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
},
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig").asText(),
)
}
private inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> {
return Array(size()) { transform(get(it)) }
}
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
fun toJson(event: Event): String = mapper.writeValueAsString(event)
fun makeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): String {
val factory = mapper.nodeFactory
val rawEvent =
factory.arrayNode(6).apply {
add(0)
add(pubKey)
add(createdAt)
add(kind)
add(
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
add(content)
}
return mapper.writeValueAsString(rawEvent)
}
fun generateId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): ByteArray {
return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
}
fun create(
signer: NostrSigner,
kind: Int,
tags: Array<Array<String>> = emptyArray(),
content: String = "",
createdAt: Long = TimeUtils.now(),
onReady: (Event) -> Unit,
) {
return signer.sign(createdAt, kind, tags, content, onReady)
}
}
}
@Immutable
open class WrappedEvent(
id: HexKey,
@JsonProperty("pubkey") pubKey: HexKey,
@JsonProperty("created_at") createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient var host: Event? = null // host event to broadcast when needed
}
@Immutable
interface AddressableEvent {
fun dTag(): String
fun address(): ATag
}
@Immutable
open class BaseAddressableEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
}
fun String.bytesUsedInMemory(): Int {
return (8 * ((((this.length) * 2) + 45) / 8))
}
data class ZapSplitSetup(
val lnAddressOrPubKeyHex: String,
val relay: String?,
val weight: Double,
val isLnAddress: Boolean,
)