package com.vitorpamplona.amethyst.service.model import com.google.gson.* import com.google.gson.annotations.SerializedName import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Secp256k1 import nostr.postr.Utils import nostr.postr.toHex import java.lang.reflect.Type import java.security.MessageDigest import java.util.* open class Event( val id: HexKey, @SerializedName("pubkey") val pubKey: HexKey, @SerializedName("created_at") val createdAt: Long, val kind: Int, val tags: List>, val content: String, val sig: HexKey ) : EventInterface { override fun id(): HexKey = id override fun pubKey(): HexKey = pubKey override fun createdAt(): Long = createdAt override fun kind(): Int = kind override fun tags(): List> = tags override fun content(): String = content override fun sig(): HexKey = sig override fun toJson(): String = gson.toJson(this) fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun hashtags() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex } override fun isTaggedHash(hashtag: String) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1).equals(hashtag, true) } /** * Checks if the ID is correct and then if the pubKey's secret key signed the event. */ override fun checkSignature() { if (!id.contentEquals(generateId())) { throw Exception( """|Unexpected ID. | Event: ${toJson()} | Actual ID: $id | Generated: ${generateId()} """.trimIndent() ) } if (!secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { throw Exception("""Bad signature!""") } } override fun hasValidSignature(): Boolean { if (!id.contentEquals(generateId())) { return false } return secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey)) } private fun generateId(): String { val rawEvent = listOf(0, pubKey, createdAt, kind, tags, content) // GSON decided to hardcode these replacements. // They break Nostr's hash check. // These lines revert their code. // https://github.com/google/gson/issues/2295 val rawEventJson = gson.toJson(rawEvent) .replace("\\u2028", "\u2028") .replace("\\u2029", "\u2029") return sha256.digest(rawEventJson.toByteArray()).toHexKey() } private class EventDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext? ): Event { val jsonObject = json.asJsonObject return Event( id = jsonObject.get("id").asString, pubKey = jsonObject.get("pubkey").asString, createdAt = jsonObject.get("created_at").asLong, kind = jsonObject.get("kind").asInt, tags = jsonObject.get("tags").asJsonArray.map { it.asJsonArray.mapNotNull { s -> if (s.isJsonNull) null else s.asString } }, content = jsonObject.get("content").asString, sig = jsonObject.get("sig").asString ) } } private class EventSerializer : JsonSerializer { override fun serialize( src: Event, typeOfSrc: Type?, context: JsonSerializationContext? ): JsonElement { return JsonObject().apply { addProperty("id", src.id) addProperty("pubkey", src.pubKey) addProperty("created_at", src.createdAt) addProperty("kind", src.kind) add( "tags", JsonArray().also { jsonTags -> src.tags.forEach { tag -> jsonTags.add( JsonArray().also { jsonTagElement -> tag.forEach { tagElement -> jsonTagElement.add(tagElement) } } ) } } ) addProperty("content", src.content) addProperty("sig", src.sig) } } } private class ByteArrayDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext? ): ByteArray = Hex.decode(json.asString) } private class ByteArraySerializer : JsonSerializer { override fun serialize( src: ByteArray, typeOfSrc: Type?, context: JsonSerializationContext? ) = JsonPrimitive(src.toHex()) } companion object { private val secp256k1 = Secp256k1.get() val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") val gson: Gson = GsonBuilder() .disableHtmlEscaping() .registerTypeAdapter(Event::class.java, EventSerializer()) .registerTypeAdapter(Event::class.java, EventDeserializer()) .registerTypeAdapter(ByteArray::class.java, ByteArraySerializer()) .registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer()) .create() fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) { 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) ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient) ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig) RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig) TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig) else -> this } fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List>, content: String): ByteArray { val rawEvent = listOf( 0, pubKey, createdAt, kind, tags, content ) val rawEventJson = gson.toJson(rawEvent) return sha256.digest(rawEventJson.toByteArray()) } fun create(privateKey: ByteArray, kind: Int, tags: List> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val id = Companion.generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey).toHexKey() return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) } } }