kopia lustrzana https://github.com/vitorpamplona/amethyst
219 wiersze
8.9 KiB
Kotlin
219 wiersze
8.9 KiB
Kotlin
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<List<String>>,
|
|
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<List<String>> = 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<Event> {
|
|
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<Event> {
|
|
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<ByteArray> {
|
|
override fun deserialize(
|
|
json: JsonElement,
|
|
typeOfT: Type?,
|
|
context: JsonDeserializationContext?
|
|
): ByteArray = Hex.decode(json.asString)
|
|
}
|
|
|
|
private class ByteArraySerializer : JsonSerializer<ByteArray> {
|
|
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<List<String>>, 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<List<String>> = 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)
|
|
}
|
|
}
|
|
}
|