Merge branch 'main' into main
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 84
|
versionCode 85
|
||||||
versionName "0.22.1"
|
versionName "0.22.2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
-keep class fr.acinq.secp256k1.jni.** { *; }
|
-keep class fr.acinq.secp256k1.jni.** { *; }
|
||||||
# For the NostrPostr library
|
# For the NostrPostr library
|
||||||
-keep class nostr.postr.** { *; }
|
-keep class nostr.postr.** { *; }
|
||||||
-keep class nostr.postr.events.** { *; }
|
-keep class com.vitorpamplona.amethyst.service.model.** { *; }
|
||||||
# Json parsing
|
# Json parsing
|
||||||
-keep class com.google.gson.reflect.** { *; }
|
-keep class com.google.gson.reflect.** { *; }
|
||||||
-keep class * extends com.google.gson.reflect.TypeToken
|
-keep class * extends com.google.gson.reflect.TypeToken
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
|||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.events.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import nostr.postr.events.Event.Companion.getRefinedEvent
|
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class LocalPreferences(context: Context) {
|
class LocalPreferences(context: Context) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Contact
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
@@ -26,16 +27,12 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.Contact
|
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.Utils
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import nostr.postr.events.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.DeletionEvent
|
||||||
import nostr.postr.events.DeletionEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import nostr.postr.events.MetadataEvent
|
|
||||||
import nostr.postr.events.PrivateDmEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import nostr.postr.toHex
|
|
||||||
|
|
||||||
val DefaultChannels = setOf(
|
val DefaultChannels = setOf(
|
||||||
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr
|
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr
|
||||||
@@ -89,10 +86,11 @@ class Account(
|
|||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
val contactList = userProfile().latestContactList
|
val contactList = userProfile().latestContactList
|
||||||
|
val follows = contactList?.follows() ?: emptyList()
|
||||||
|
|
||||||
if (contactList != null && contactList.follows.size > 0) {
|
if (contactList != null && follows.isNotEmpty()) {
|
||||||
val event = ContactListEvent.create(
|
val event = ContactListEvent.create(
|
||||||
contactList.follows,
|
follows,
|
||||||
relays,
|
relays,
|
||||||
loggedIn.privKey!!)
|
loggedIn.privKey!!)
|
||||||
|
|
||||||
@@ -111,14 +109,7 @@ class Account(
|
|||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
loggedIn.privKey?.let {
|
loggedIn.privKey?.let {
|
||||||
val createdAt = Date().time / 1000
|
val event = MetadataEvent.create(toString, loggedIn.privKey!!)
|
||||||
val content = toString
|
|
||||||
val pubKey = Utils.pubkeyCreate(it)
|
|
||||||
val tags = listOf<List<String>>()
|
|
||||||
val id = Event.generateId(pubKey, createdAt, MetadataEvent.kind, tags, content)
|
|
||||||
val sig = Utils.sign(id, it)
|
|
||||||
val event = MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
|
||||||
|
|
||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
}
|
}
|
||||||
@@ -250,10 +241,11 @@ class Account(
|
|||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
val contactList = userProfile().latestContactList
|
val contactList = userProfile().latestContactList
|
||||||
|
val follows = contactList?.follows() ?: emptyList()
|
||||||
|
|
||||||
val event = if (contactList != null && contactList.follows.size > 0) {
|
val event = if (contactList != null && follows.isNotEmpty()) {
|
||||||
ContactListEvent.create(
|
ContactListEvent.create(
|
||||||
contactList.follows.plus(Contact(user.pubkeyHex, null)),
|
follows.plus(Contact(user.pubkeyHex, null)),
|
||||||
userProfile().relays,
|
userProfile().relays,
|
||||||
loggedIn.privKey!!)
|
loggedIn.privKey!!)
|
||||||
} else {
|
} else {
|
||||||
@@ -273,10 +265,11 @@ class Account(
|
|||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
val contactList = userProfile().latestContactList
|
val contactList = userProfile().latestContactList
|
||||||
|
val follows = contactList?.follows() ?: emptyList()
|
||||||
|
|
||||||
if (contactList != null && contactList.follows.size > 0) {
|
if (contactList != null && follows.isNotEmpty()) {
|
||||||
val event = ContactListEvent.create(
|
val event = ContactListEvent.create(
|
||||||
contactList.follows.filter { it.pubKeyHex != user.pubkeyHex },
|
follows.filter { it.pubKeyHex != user.pubkeyHex },
|
||||||
userProfile().relays,
|
userProfile().relays,
|
||||||
loggedIn.privKey!!)
|
loggedIn.privKey!!)
|
||||||
|
|
||||||
@@ -320,37 +313,6 @@ class Account(
|
|||||||
LocalCache.consume(signedEvent, null)
|
LocalCache.consume(signedEvent, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPrivateMessageWithReply(
|
|
||||||
recipientPubKey: ByteArray,
|
|
||||||
msg: String,
|
|
||||||
replyTos: List<String>? = null, mentions: List<String>? = null,
|
|
||||||
privateKey: ByteArray,
|
|
||||||
createdAt: Long = Date().time / 1000,
|
|
||||||
publishedRecipientPubKey: ByteArray? = null,
|
|
||||||
advertiseNip18: Boolean = true
|
|
||||||
): PrivateDmEvent {
|
|
||||||
val content = Utils.encrypt(
|
|
||||||
if (advertiseNip18) {
|
|
||||||
PrivateDmEvent.nip18Advertisement
|
|
||||||
} else { "" } + msg,
|
|
||||||
privateKey,
|
|
||||||
recipientPubKey)
|
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
|
||||||
val tags = mutableListOf<List<String>>()
|
|
||||||
publishedRecipientPubKey?.let {
|
|
||||||
tags.add(listOf("p", publishedRecipientPubKey.toHex()))
|
|
||||||
}
|
|
||||||
replyTos?.forEach {
|
|
||||||
tags.add(listOf("e", it))
|
|
||||||
}
|
|
||||||
mentions?.forEach {
|
|
||||||
tags.add(listOf("p", it))
|
|
||||||
}
|
|
||||||
val id = Event.generateId(pubKey, createdAt, PrivateDmEvent.kind, tags, content)
|
|
||||||
val sig = Utils.sign(id, privateKey)
|
|
||||||
return PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) {
|
fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
val user = LocalCache.users[toUser] ?: return
|
val user = LocalCache.users[toUser] ?: return
|
||||||
@@ -358,7 +320,7 @@ class Account(
|
|||||||
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||||
val mentionsHex = emptyList<String>()
|
val mentionsHex = emptyList<String>()
|
||||||
|
|
||||||
val signedEvent = createPrivateMessageWithReply(
|
val signedEvent = PrivateDmEvent.create(
|
||||||
recipientPubKey = user.pubkey(),
|
recipientPubKey = user.pubkey(),
|
||||||
publishedRecipientPubKey = user.pubkey(),
|
publishedRecipientPubKey = user.pubkey(),
|
||||||
msg = message,
|
msg = message,
|
||||||
@@ -386,7 +348,7 @@ class Account(
|
|||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
|
|
||||||
joinChannel(event.id.toHex())
|
joinChannel(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun joinChannel(idHex: String) {
|
fun joinChannel(idHex: String) {
|
||||||
@@ -438,7 +400,7 @@ class Account(
|
|||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
|
|
||||||
joinChannel(event.id.toHex())
|
joinChannel(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptContent(note: Note): String? {
|
fun decryptContent(note: Note): String? {
|
||||||
@@ -446,26 +408,12 @@ class Account(
|
|||||||
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
|
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
|
||||||
var pubkeyToUse = event.pubKey
|
var pubkeyToUse = event.pubKey
|
||||||
|
|
||||||
val recepientPK = event.recipientPubKey
|
val recepientPK = event.recipientPubKey()
|
||||||
|
|
||||||
if (note.author == userProfile() && recepientPK != null)
|
if (note.author == userProfile() && recepientPK != null)
|
||||||
pubkeyToUse = recepientPK
|
pubkeyToUse = recepientPK
|
||||||
|
|
||||||
return try {
|
event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray())
|
||||||
val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse)
|
|
||||||
|
|
||||||
val retVal = Utils.decrypt(event.content, sharedSecret)
|
|
||||||
|
|
||||||
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
|
|
||||||
retVal.substring(16)
|
|
||||||
} else {
|
|
||||||
retVal
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
event?.content
|
event?.content
|
||||||
}
|
}
|
||||||
@@ -495,10 +443,10 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateContactListTo(newContactList: ContactListEvent?) {
|
private fun updateContactListTo(newContactList: ContactListEvent?) {
|
||||||
if (newContactList?.follows.isNullOrEmpty()) return
|
if (newContactList?.follows().isNullOrEmpty()) return
|
||||||
|
|
||||||
// Events might be different objects, we have to compare their ids.
|
// Events might be different objects, we have to compare their ids.
|
||||||
if (backupContactList?.id?.toHex() != newContactList?.id?.toHex()) {
|
if (backupContactList?.id != newContactList?.id) {
|
||||||
backupContactList = newContactList
|
backupContactList = newContactList
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>)
|
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>)
|
||||||
@@ -22,7 +22,7 @@ class AntiSpamFilter {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun isSpam(event: Event): Boolean {
|
fun isSpam(event: Event): Boolean {
|
||||||
val idHex = event.id.toHexKey()
|
val idHex = event.id
|
||||||
|
|
||||||
// if already processed, ok
|
// if already processed, ok
|
||||||
if (LocalCache.notes[idHex] != null) return false
|
if (LocalCache.notes[idHex] != null) return false
|
||||||
@@ -37,15 +37,15 @@ class AntiSpamFilter {
|
|||||||
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
||||||
|
|
||||||
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
|
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
|
||||||
Log.w("Potential SPAM Message", "${event.id.toHex()} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
|
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
|
||||||
|
|
||||||
// Log down offenders
|
// Log down offenders
|
||||||
if (spamMessages.get(hash) == null) {
|
if (spamMessages.get(hash) == null) {
|
||||||
spamMessages.put(hash, Spammer(event.pubKey.toHexKey(), setOf(recentMessages[hash], event.id.toHex())))
|
spamMessages.put(hash, Spammer(event.pubKey, setOf(recentMessages[hash], event.id)))
|
||||||
liveSpam.invalidateData()
|
liveSpam.invalidateData()
|
||||||
} else {
|
} else {
|
||||||
val spammer = spamMessages.get(hash)
|
val spammer = spamMessages.get(hash)
|
||||||
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id.toHex()
|
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
|
||||||
|
|
||||||
liveSpam.invalidateData()
|
liveSpam.invalidateData()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.DeletionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
@@ -32,13 +39,6 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.events.ContactListEvent
|
|
||||||
import nostr.postr.events.DeletionEvent
|
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.events.MetadataEvent
|
|
||||||
import nostr.postr.events.PrivateDmEvent
|
|
||||||
import nostr.postr.events.RecommendRelayEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ object LocalCache {
|
|||||||
|
|
||||||
fun consume(event: MetadataEvent) {
|
fun consume(event: MetadataEvent) {
|
||||||
// new event
|
// new event
|
||||||
val oldUser = getOrCreateUser(event.pubKey.toHexKey())
|
val oldUser = getOrCreateUser(event.pubKey)
|
||||||
if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) {
|
if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) {
|
||||||
val newUser = try {
|
val newUser = try {
|
||||||
metadataParser.readValue(
|
metadataParser.readValue(
|
||||||
@@ -173,8 +173,8 @@ object LocalCache {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
if (relay != null) {
|
if (relay != null) {
|
||||||
author.addRelayBeingUsed(relay, event.createdAt)
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
@@ -220,7 +220,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
if (relay != null) {
|
if (relay != null) {
|
||||||
author.addRelayBeingUsed(relay, event.createdAt)
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
@@ -228,7 +228,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event?.id?.toHex() == event.id.toHex()) return
|
if (note.event?.id == event.id) return
|
||||||
|
|
||||||
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
||||||
@@ -284,14 +284,15 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ContactListEvent) {
|
fun consume(event: ContactListEvent) {
|
||||||
val user = getOrCreateUser(event.pubKey.toHexKey())
|
val user = getOrCreateUser(event.pubKey)
|
||||||
|
val follows = event.follows()
|
||||||
|
|
||||||
if (event.createdAt > user.updatedFollowsAt && event.follows.isNotEmpty()) {
|
if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) {
|
||||||
// Saves relay list only if it's a user that is currently been seen
|
// Saves relay list only if it's a user that is currently been seen
|
||||||
user.latestContactList = event
|
user.latestContactList = event
|
||||||
|
|
||||||
user.updateFollows(
|
user.updateFollows(
|
||||||
event.follows.map {
|
follows.map {
|
||||||
try {
|
try {
|
||||||
val pubKey = decodePublicKey(it.pubKeyHex)
|
val pubKey = decodePublicKey(it.pubKeyHex)
|
||||||
getOrCreateUser(pubKey.toHexKey())
|
getOrCreateUser(pubKey.toHexKey())
|
||||||
@@ -316,20 +317,17 @@ object LocalCache {
|
|||||||
user.updateRelays(relays)
|
user.updateRelays(relays)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("relay import issue")
|
Log.w("Relay List Parser","Relay import issue ${e.message}", e)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(
|
Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
|
||||||
"CL",
|
|
||||||
"AAA ${user.toBestDisplayName()} ${event.follows.size}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: PrivateDmEvent, relay: Relay?) {
|
fun consume(event: PrivateDmEvent, relay: Relay?) {
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
if (relay != null) {
|
if (relay != null) {
|
||||||
author.addRelayBeingUsed(relay, event.createdAt)
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
@@ -339,7 +337,7 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val recipient = event.recipientPubKey?.let { getOrCreateUser(it.toHexKey()) }
|
val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) }
|
||||||
|
|
||||||
//Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
//Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||||
|
|
||||||
@@ -359,9 +357,9 @@ object LocalCache {
|
|||||||
fun consume(event: DeletionEvent) {
|
fun consume(event: DeletionEvent) {
|
||||||
var deletedAtLeastOne = false
|
var deletedAtLeastOne = false
|
||||||
|
|
||||||
event.deleteEvents.mapNotNull { notes[it] }.forEach { deleteNote ->
|
event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote ->
|
||||||
// must be the same author
|
// must be the same author
|
||||||
if (deleteNote.author?.pubkeyHex == event.pubKey.toHexKey()) {
|
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||||
deleteNote.author?.removeNote(deleteNote)
|
deleteNote.author?.removeNote(deleteNote)
|
||||||
|
|
||||||
// reverts the add
|
// reverts the add
|
||||||
@@ -395,14 +393,14 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: RepostEvent) {
|
fun consume(event: RepostEvent) {
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
@@ -429,12 +427,12 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ReactionEvent) {
|
fun consume(event: ReactionEvent) {
|
||||||
val note = getOrCreateNote(event.id.toHexKey())
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
@@ -475,8 +473,8 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ReportEvent, relay: Relay?) {
|
fun consume(event: ReportEvent, relay: Relay?) {
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
if (relay != null) {
|
if (relay != null) {
|
||||||
author.addRelayBeingUsed(relay, event.createdAt)
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
@@ -507,13 +505,13 @@ object LocalCache {
|
|||||||
fun consume(event: ChannelCreateEvent) {
|
fun consume(event: ChannelCreateEvent) {
|
||||||
//Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
|
//Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
|
||||||
// new event
|
// new event
|
||||||
val oldChannel = getOrCreateChannel(event.id.toHex())
|
val oldChannel = getOrCreateChannel(event.id)
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||||
if (oldChannel.creator == null || oldChannel.creator == author) {
|
if (oldChannel.creator == null || oldChannel.creator == author) {
|
||||||
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
oldChannel.addNote(note)
|
oldChannel.addNote(note)
|
||||||
note.loadEvent(event, author, emptyList(), emptyList())
|
note.loadEvent(event, author, emptyList(), emptyList())
|
||||||
|
|
||||||
@@ -530,12 +528,12 @@ object LocalCache {
|
|||||||
|
|
||||||
// new event
|
// new event
|
||||||
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
|
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||||
if (oldChannel.creator == null || oldChannel.creator == author) {
|
if (oldChannel.creator == null || oldChannel.creator == author) {
|
||||||
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
oldChannel.addNote(note)
|
oldChannel.addNote(note)
|
||||||
note.loadEvent(event, author, emptyList(), emptyList())
|
note.loadEvent(event, author, emptyList(), emptyList())
|
||||||
|
|
||||||
@@ -559,10 +557,10 @@ object LocalCache {
|
|||||||
|
|
||||||
val channel = checkGetOrCreateChannel(channelId) ?: return
|
val channel = checkGetOrCreateChannel(channelId) ?: return
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id)
|
||||||
channel.addNote(note)
|
channel.addNote(note)
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
if (relay != null) {
|
if (relay != null) {
|
||||||
author.addRelayBeingUsed(relay, event.createdAt)
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
@@ -606,14 +604,14 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: LnZapEvent) {
|
fun consume(event: LnZapEvent) {
|
||||||
val note = getOrCreateNote(event.id.toHexKey())
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) }
|
val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) }
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
||||||
@@ -645,15 +643,15 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: LnZapRequestEvent) {
|
fun consume(event: LnZapRequestEvent) {
|
||||||
val note = getOrCreateNote(event.id.toHexKey())
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey)
|
||||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
note.loadEvent(event, author, mentions, repliesTo)
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
|
||||||
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ open class Note(val idHex: String) {
|
|||||||
val channelHex =
|
val channelHex =
|
||||||
(event as? ChannelMessageEvent)?.channel() ?:
|
(event as? ChannelMessageEvent)?.channel() ?:
|
||||||
(event as? ChannelMetadataEvent)?.channel() ?:
|
(event as? ChannelMetadataEvent)?.channel() ?:
|
||||||
(event as? ChannelCreateEvent)?.let { it.id.toHexKey() }
|
(event as? ChannelCreateEvent)?.let { it.id }
|
||||||
|
|
||||||
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
|
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
}
|
}
|
||||||
@@ -251,6 +251,22 @@ open class Note(val idHex: String) {
|
|||||||
}?.isNotEmpty() ?: false)
|
}?.isNotEmpty() ?: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun directlyCiteUsersHex(): Set<HexKey> {
|
||||||
|
val matcher = tagSearch.matcher(event?.content ?: "")
|
||||||
|
val returningList = mutableSetOf<String>()
|
||||||
|
while (matcher.find()) {
|
||||||
|
try {
|
||||||
|
val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) }
|
||||||
|
if (tag != null && tag[0] == "p") {
|
||||||
|
returningList.add(tag[1])
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returningList
|
||||||
|
}
|
||||||
|
|
||||||
fun directlyCiteUsers(): Set<User> {
|
fun directlyCiteUsers(): Set<User> {
|
||||||
val matcher = tagSearch.matcher(event?.content ?: "")
|
val matcher = tagSearch.matcher(event?.content ?: "")
|
||||||
val returningList = mutableSetOf<User>()
|
val returningList = mutableSetOf<User>()
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.Bech32
|
import nostr.postr.Bech32
|
||||||
import nostr.postr.events.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import nostr.postr.events.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
|
|
||||||
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
|
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
|
||||||
|
|||||||
@@ -1,71 +1,103 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.service.model.ATag
|
import nostr.postr.bechToBytes
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import nostr.postr.Bech32
|
|
||||||
import nostr.postr.bechToBytes
|
|
||||||
import nostr.postr.toByteArray
|
|
||||||
|
|
||||||
class Nip19 {
|
class Nip19 {
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
USER, NOTE, RELAY, ADDRESS
|
USER, NOTE, RELAY, ADDRESS
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Return(val type: Type, val hex: String)
|
data class Return(val type: Type, val hex: String)
|
||||||
|
|
||||||
fun uriToRoute(uri: String?): Return? {
|
fun uriToRoute(uri: String?): Return? {
|
||||||
try {
|
try {
|
||||||
val key = uri?.removePrefix("nostr:")
|
val key = uri?.removePrefix("nostr:") ?: return null
|
||||||
|
|
||||||
if (key != null) {
|
val bytes = key.bechToBytes()
|
||||||
val bytes = key.bechToBytes()
|
if (key.startsWith("npub")) {
|
||||||
if (key.startsWith("npub")) {
|
return npub(bytes)
|
||||||
return Return(Type.USER, bytes.toHexKey())
|
} else if (key.startsWith("note")) {
|
||||||
}
|
return note(bytes)
|
||||||
if (key.startsWith("note")) {
|
} else if (key.startsWith("nprofile")) {
|
||||||
return Return(Type.NOTE, bytes.toHexKey())
|
return nprofile(bytes)
|
||||||
}
|
} else if (key.startsWith("nevent")) {
|
||||||
if (key.startsWith("nprofile")) {
|
return nevent(bytes)
|
||||||
val tlv = parseTLV(bytes)
|
} else if (key.startsWith("nrelay")) {
|
||||||
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
|
return nrelay(bytes)
|
||||||
if (hex != null)
|
} else if (key.startsWith("naddr")) {
|
||||||
return Return(Type.USER, hex)
|
return naddr(bytes)
|
||||||
}
|
|
||||||
if (key.startsWith("nevent")) {
|
|
||||||
val tlv = parseTLV(bytes)
|
|
||||||
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
|
|
||||||
if (hex != null)
|
|
||||||
return Return(Type.USER, hex)
|
|
||||||
}
|
|
||||||
if (key.startsWith("nrelay")) {
|
|
||||||
val tlv = parseTLV(bytes)
|
|
||||||
val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
|
|
||||||
if (relayUrl != null)
|
|
||||||
return Return(Type.RELAY, relayUrl)
|
|
||||||
}
|
|
||||||
if (key.startsWith("naddr")) {
|
|
||||||
val tlv = parseTLV(bytes)
|
|
||||||
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
|
|
||||||
val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
|
|
||||||
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
|
|
||||||
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
|
|
||||||
if (d != null)
|
|
||||||
return Return(Type.ADDRESS, "$kind:$author:$d")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
|
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
|
||||||
//e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun npub(bytes: ByteArray): Return {
|
||||||
|
return Return(Type.USER, bytes.toHexKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun note(bytes: ByteArray): Return {
|
||||||
|
return Return(Type.NOTE, bytes.toHexKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nprofile(bytes: ByteArray): Return? {
|
||||||
|
val hex = parseTLV(bytes)
|
||||||
|
.get(NIP19TLVTypes.SPECIAL.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toHexKey() ?: return null
|
||||||
|
|
||||||
|
return Return(Type.USER, hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nevent(bytes: ByteArray): Return? {
|
||||||
|
val hex = parseTLV(bytes)
|
||||||
|
.get(NIP19TLVTypes.SPECIAL.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toHexKey() ?: return null
|
||||||
|
|
||||||
|
return Return(Type.USER, hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nrelay(bytes: ByteArray): Return? {
|
||||||
|
val relayUrl = parseTLV(bytes)
|
||||||
|
.get(NIP19TLVTypes.SPECIAL.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toString(Charsets.UTF_8) ?: return null
|
||||||
|
|
||||||
|
return Return(Type.RELAY, relayUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun naddr(bytes: ByteArray): Return? {
|
||||||
|
val tlv = parseTLV(bytes)
|
||||||
|
|
||||||
|
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toString(Charsets.UTF_8) ?: return null
|
||||||
|
|
||||||
|
val relay = tlv.get(NIP19TLVTypes.RELAY.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toString(Charsets.UTF_8)
|
||||||
|
|
||||||
|
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toHexKey()
|
||||||
|
|
||||||
|
val kind = tlv.get(NIP19TLVTypes.KIND.id)
|
||||||
|
?.get(0)
|
||||||
|
?.let { toInt32(it) }
|
||||||
|
|
||||||
|
return Return(Type.ADDRESS, "$kind:$author:$d")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin
|
// Classes should start with an uppercase letter in kotlin
|
||||||
|
enum class NIP19TLVTypes(val id: Byte) {
|
||||||
SPECIAL(0),
|
SPECIAL(0),
|
||||||
RELAY(1),
|
RELAY(1),
|
||||||
AUTHOR(2),
|
AUTHOR(2),
|
||||||
@@ -78,19 +110,19 @@ fun toInt32(bytes: ByteArray): Int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
|
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
|
||||||
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
val result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
||||||
var rest = data
|
var rest = data
|
||||||
while (rest.isNotEmpty()) {
|
while (rest.isNotEmpty()) {
|
||||||
val t = rest[0]
|
val t = rest[0]
|
||||||
val l = rest[1]
|
val l = rest[1]
|
||||||
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
|
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
|
||||||
rest = rest.sliceArray(IntRange(2 + l, rest.size-1))
|
rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
|
||||||
if (v.size < l) continue
|
if (v.size < l) continue
|
||||||
|
|
||||||
if (!result.containsKey(t)) {
|
if (!result.containsKey(t)) {
|
||||||
result.put(t, mutableListOf())
|
result[t] = mutableListOf()
|
||||||
}
|
}
|
||||||
result.get(t)?.add(v)
|
result[t]?.add(v)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import nostr.postr.events.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object NostrAccountDataSource: NostrDataSource("AccountData") {
|
object NostrAccountDataSource: NostrDataSource("AccountData") {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
|
||||||
object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") {
|
object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
|
||||||
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
|
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
||||||
@@ -15,7 +16,6 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.service.relays.Subscription
|
import com.vitorpamplona.amethyst.service.relays.Subscription
|
||||||
import com.vitorpamplona.amethyst.service.relays.hasValidSignature
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -26,12 +26,12 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nostr.postr.events.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import nostr.postr.events.DeletionEvent
|
import com.vitorpamplona.amethyst.service.model.DeletionEvent
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import nostr.postr.events.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import nostr.postr.events.RecommendRelayEvent
|
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
abstract class NostrDataSource(val debugName: String) {
|
abstract class NostrDataSource(val debugName: String) {
|
||||||
@@ -62,39 +62,32 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MetadataEvent -> LocalCache.consume(event)
|
is ChannelCreateEvent -> LocalCache.consume(event)
|
||||||
//is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote
|
is ChannelHideMessageEvent -> LocalCache.consume(event)
|
||||||
is RecommendRelayEvent -> LocalCache.consume(event)
|
is ChannelMessageEvent -> LocalCache.consume(event, relay)
|
||||||
|
is ChannelMetadataEvent -> LocalCache.consume(event)
|
||||||
|
is ChannelMuteUserEvent -> LocalCache.consume(event)
|
||||||
is ContactListEvent -> LocalCache.consume(event)
|
is ContactListEvent -> LocalCache.consume(event)
|
||||||
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
|
||||||
is DeletionEvent -> LocalCache.consume(event)
|
is DeletionEvent -> LocalCache.consume(event)
|
||||||
else -> when (event.kind) {
|
|
||||||
TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
|
||||||
|
|
||||||
RepostEvent.kind -> {
|
is LnZapEvent -> {
|
||||||
val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
||||||
|
LocalCache.consume(event)
|
||||||
repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
}
|
||||||
LocalCache.consume(repostEvent)
|
is LnZapRequestEvent -> LocalCache.consume(event)
|
||||||
}
|
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
||||||
ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
is MetadataEvent -> LocalCache.consume(event)
|
||||||
ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
||||||
|
is ReactionEvent -> LocalCache.consume(event)
|
||||||
LnZapEvent.kind -> {
|
is RecommendRelayEvent -> LocalCache.consume(event)
|
||||||
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
is ReportEvent -> LocalCache.consume(event, relay)
|
||||||
|
is RepostEvent -> {
|
||||||
zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
||||||
LocalCache.consume(zapEvent)
|
LocalCache.consume(event)
|
||||||
}
|
}
|
||||||
LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
is TextNoteEvent -> LocalCache.consume(event, relay)
|
||||||
|
else -> {
|
||||||
ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
Log.w("Event Not Supported", event.toJson())
|
||||||
ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
|
||||||
ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
|
||||||
ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
|
||||||
ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
|
||||||
|
|
||||||
LongTextNoteEvent.kind -> LocalCache.consume(LongTextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
|
|||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
import nostr.postr.events.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
|
object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
|
|
||||||
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
|
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
|
||||||
var usersToWatch = setOf<User>()
|
var usersToWatch = setOf<User>()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import nostr.postr.events.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
|
object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
|
|||||||
|
|
||||||
val fullArray =
|
val fullArray =
|
||||||
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
|
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
|
||||||
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
|
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
|
||||||
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
|
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
|
||||||
|
|
||||||
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
|
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
|
|||||||
Hex.decode(parts[1])
|
Hex.decode(parts[1])
|
||||||
ATag(parts[0].toInt(), parts[1], parts[2])
|
ATag(parts[0].toInt(), parts[1], parts[2])
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}")
|
Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
println("Issue trying to Decode NIP19 ${this}: ${e.message}")
|
Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}")
|
||||||
//e.printStackTrace()
|
//e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.events.MetadataEvent
|
|
||||||
|
|
||||||
class ChannelCreateEvent (
|
class ChannelCreateEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun channelInfo() = try {
|
fun channelInfo() = try {
|
||||||
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
|
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
|
||||||
@@ -35,11 +35,11 @@ class ChannelCreateEvent (
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = emptyList<List<String>>()
|
val tags = emptyList<List<String>>()
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
|
return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
|
|
||||||
class ChannelHideMessageEvent (
|
class ChannelHideMessageEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ class ChannelHideMessageEvent (
|
|||||||
|
|
||||||
fun create(reason: String, messagesToHide: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent {
|
fun create(reason: String, messagesToHide: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent {
|
||||||
val content = reason
|
val content = reason
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags =
|
val tags =
|
||||||
messagesToHide?.map {
|
messagesToHide?.map {
|
||||||
listOf("e", it)
|
listOf("e", it)
|
||||||
@@ -27,7 +28,7 @@ class ChannelHideMessageEvent (
|
|||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
|
|
||||||
class ChannelMessageEvent (
|
class ChannelMessageEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||||
@@ -22,7 +23,7 @@ class ChannelMessageEvent (
|
|||||||
|
|
||||||
fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent {
|
fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent {
|
||||||
val content = message
|
val content = message
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = mutableListOf(
|
val tags = mutableListOf(
|
||||||
listOf("e", channel, "", "root")
|
listOf("e", channel, "", "root")
|
||||||
)
|
)
|
||||||
@@ -35,7 +36,7 @@ class ChannelMessageEvent (
|
|||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.events.MetadataEvent
|
|
||||||
|
|
||||||
class ChannelMetadataEvent (
|
class ChannelMetadataEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||||
fun channelInfo() =
|
fun channelInfo() =
|
||||||
@@ -33,11 +33,11 @@ class ChannelMetadataEvent (
|
|||||||
else
|
else
|
||||||
""
|
""
|
||||||
|
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = listOf( listOf("e", originalChannelIdHex, "", "root") )
|
val tags = listOf( listOf("e", originalChannelIdHex, "", "root") )
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
|
|
||||||
class ChannelMuteUserEvent (
|
class ChannelMuteUserEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 44
|
const val kind = 44
|
||||||
|
|
||||||
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent {
|
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent {
|
||||||
val content = reason
|
val content = reason
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags =
|
val tags =
|
||||||
usersToMute?.map {
|
usersToMute?.map {
|
||||||
listOf("p", it)
|
listOf("p", it)
|
||||||
@@ -29,7 +29,7 @@ class ChannelMuteUserEvent (
|
|||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
|
return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
data class Contact(val pubKeyHex: String, val relayUri: String?)
|
||||||
|
|
||||||
|
class ContactListEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun follows() = try {
|
||||||
|
tags.filter { it[0] == "p" }.map { Contact(it[1], it.getOrNull(2)) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ContactListEvent", "can't parse tags as follows: $tags", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun relayUse() = try {
|
||||||
|
if (content.isNotEmpty())
|
||||||
|
gson.fromJson(content, object: TypeToken<Map<String, ReadWrite>>() {}.type)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ContactListEvent", "can't parse content as relay lists: $tags", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 3
|
||||||
|
|
||||||
|
fun create(follows: List<Contact>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent {
|
||||||
|
val content = if (relayUse != null)
|
||||||
|
gson.toJson(relayUse)
|
||||||
|
else
|
||||||
|
""
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = follows.map {
|
||||||
|
if (it.relayUri != null)
|
||||||
|
listOf("p", it.pubKeyHex, it.relayUri)
|
||||||
|
else
|
||||||
|
listOf("p", it.pubKeyHex)
|
||||||
|
}
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReadWrite(val read: Boolean, val write: Boolean)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
class DeletionEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun deleteEvents() = tags.map { it[1] }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 5
|
||||||
|
|
||||||
|
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent {
|
||||||
|
val content = ""
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = deleteEvents.map { listOf("e", it) }
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonDeserializationContext
|
||||||
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonPrimitive
|
||||||
|
import com.google.gson.JsonSerializationContext
|
||||||
|
import com.google.gson.JsonSerializer
|
||||||
|
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 java.lang.reflect.Type
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import nostr.postr.toHex
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
fun toJson(): String = gson.toJson(this)
|
||||||
|
|
||||||
|
fun generateId(): String {
|
||||||
|
val rawEvent = listOf(
|
||||||
|
0,
|
||||||
|
pubKey,
|
||||||
|
createdAt,
|
||||||
|
kind,
|
||||||
|
tags,
|
||||||
|
content
|
||||||
|
)
|
||||||
|
val rawEventJson = gson.toJson(rawEvent)
|
||||||
|
return sha256.digest(rawEventJson.toByteArray()).toHexKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the ID is correct and then if the pubKey's secret key signed the event.
|
||||||
|
*/
|
||||||
|
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!""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasValidSignature(): Boolean {
|
||||||
|
if (!id.contentEquals(generateId())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!Secp256k1.get().verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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.map { s -> s.asString }
|
||||||
|
},
|
||||||
|
content = jsonObject.get("content").asString,
|
||||||
|
sig = jsonObject.get("sig").asString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ByteArrayDeserializer : JsonDeserializer<ByteArray> {
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type?,
|
||||||
|
context: JsonDeserializationContext?
|
||||||
|
): ByteArray = Hex.decode(json.asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import java.math.BigDecimal
|
|
||||||
import nostr.postr.events.Event
|
|
||||||
|
|
||||||
class LnZapEvent (
|
class LnZapEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class LnZapRequestEvent (
|
class LnZapRequestEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
@@ -22,10 +23,10 @@ class LnZapRequestEvent (
|
|||||||
|
|
||||||
fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
|
fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
|
||||||
val content = ""
|
val content = ""
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
var tags = listOf(
|
var tags = listOf(
|
||||||
listOf("e", originalNote.id.toHex()),
|
listOf("e", originalNote.id),
|
||||||
listOf("p", originalNote.pubKey.toHex()),
|
listOf("p", originalNote.pubKey),
|
||||||
listOf("relays") + relays
|
listOf("relays") + relays
|
||||||
)
|
)
|
||||||
if (originalNote is LongTextNoteEvent) {
|
if (originalNote is LongTextNoteEvent) {
|
||||||
@@ -34,19 +35,19 @@ class LnZapRequestEvent (
|
|||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(userHex: String, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
|
fun create(userHex: String, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
|
||||||
val content = ""
|
val content = ""
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = listOf(
|
val tags = listOf(
|
||||||
listOf("p", userHex),
|
listOf("p", userHex),
|
||||||
listOf("relays") + relays
|
listOf("relays") + relays
|
||||||
)
|
)
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
|
|
||||||
class LongTextNoteEvent(
|
class LongTextNoteEvent(
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
fun address() = ATag(kind, pubKey.toHexKey(), dTag())
|
fun address() = ATag(kind, pubKey, dTag())
|
||||||
|
|
||||||
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
@@ -34,7 +34,7 @@ class LongTextNoteEvent(
|
|||||||
const val kind = 30023
|
const val kind = 30023
|
||||||
|
|
||||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
|
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
replyTos?.forEach {
|
replyTos?.forEach {
|
||||||
tags.add(listOf("e", it))
|
tags.add(listOf("e", it))
|
||||||
@@ -44,7 +44,7 @@ class LongTextNoteEvent(
|
|||||||
}
|
}
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
data class ContactMetaData(
|
||||||
|
val name: String,
|
||||||
|
val picture: String,
|
||||||
|
val about: String,
|
||||||
|
val nip05: String?)
|
||||||
|
|
||||||
|
class MetadataEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun contactMetaData() = try {
|
||||||
|
gson.fromJson(content, ContactMetaData::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MetadataEvent", "Can't parse $content", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 0
|
||||||
|
val gson = Gson()
|
||||||
|
|
||||||
|
fun create(contactMetaData: ContactMetaData, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
|
||||||
|
return create(gson.toJson(contactMetaData), privateKey, createdAt = createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(contactMetaData: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
|
||||||
|
val content = contactMetaData
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = listOf<List<String>>()
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import fr.acinq.secp256k1.Hex
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import nostr.postr.toHex
|
||||||
|
|
||||||
|
class PrivateDmEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
/**
|
||||||
|
* This may or may not be the actual recipient's pub key. The event is intended to look like a
|
||||||
|
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
|
||||||
|
* for initial messages.
|
||||||
|
*/
|
||||||
|
fun recipientPubKey() = tags.firstOrNull { it.firstOrNull() == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
|
||||||
|
*
|
||||||
|
* Nip-18 messages should refer to other events by inline references in the content like
|
||||||
|
* `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506).
|
||||||
|
*/
|
||||||
|
fun replyTo() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
|
||||||
|
|
||||||
|
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
|
||||||
|
return try {
|
||||||
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
|
||||||
|
|
||||||
|
val retVal = Utils.decrypt(content, sharedSecret)
|
||||||
|
|
||||||
|
if (retVal.startsWith(nip18Advertisement)) {
|
||||||
|
retVal.substring(16)
|
||||||
|
} else {
|
||||||
|
retVal
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("PrivateDM", "Error decrypting the message ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 4
|
||||||
|
|
||||||
|
const val nip18Advertisement = "[//]: # (nip18)\n"
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
recipientPubKey: ByteArray,
|
||||||
|
msg: String,
|
||||||
|
replyTos: List<String>? = null, mentions: List<String>? = null,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = Date().time / 1000,
|
||||||
|
publishedRecipientPubKey: ByteArray? = null,
|
||||||
|
advertiseNip18: Boolean = true
|
||||||
|
): PrivateDmEvent {
|
||||||
|
val content = Utils.encrypt(
|
||||||
|
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
|
||||||
|
privateKey,
|
||||||
|
recipientPubKey)
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
publishedRecipientPubKey?.let {
|
||||||
|
tags.add(listOf("p", publishedRecipientPubKey.toHex()))
|
||||||
|
}
|
||||||
|
replyTos?.forEach {
|
||||||
|
tags.add(listOf("e", it))
|
||||||
|
}
|
||||||
|
mentions?.forEach {
|
||||||
|
tags.add(listOf("p", it))
|
||||||
|
}
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class ReactionEvent (
|
class ReactionEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
@@ -30,16 +31,16 @@ class ReactionEvent (
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
|
||||||
var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex()))
|
var tags = listOf( listOf("e", originalNote.id), listOf("p", originalNote.pubKey))
|
||||||
if (originalNote is LongTextNoteEvent) {
|
if (originalNote is LongTextNoteEvent) {
|
||||||
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
|
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
class RecommendRelayEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
val lenient: Boolean = false
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
fun relay() = if (lenient)
|
||||||
|
URI.create(content.trim())
|
||||||
|
else
|
||||||
|
URI.create(content)
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 2
|
||||||
|
|
||||||
|
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RecommendRelayEvent {
|
||||||
|
val content = relay.toString()
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = listOf<List<String>>()
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
|
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
|
||||||
|
|
||||||
// NIP 56 event.
|
// NIP 56 event.
|
||||||
class ReportEvent (
|
class ReportEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
private fun defaultReportType(): ReportType {
|
private fun defaultReportType(): ReportType {
|
||||||
@@ -55,10 +56,10 @@ class ReportEvent (
|
|||||||
fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
||||||
val content = ""
|
val content = ""
|
||||||
|
|
||||||
val reportPostTag = listOf("e", reportedPost.id.toHex(), type.name.toLowerCase())
|
val reportPostTag = listOf("e", reportedPost.id, type.name.toLowerCase())
|
||||||
val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase())
|
val reportAuthorTag = listOf("p", reportedPost.pubKey, type.name.toLowerCase())
|
||||||
|
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
|
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
|
||||||
|
|
||||||
if (reportedPost is LongTextNoteEvent) {
|
if (reportedPost is LongTextNoteEvent) {
|
||||||
@@ -67,7 +68,7 @@ class ReportEvent (
|
|||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
||||||
@@ -75,11 +76,11 @@ class ReportEvent (
|
|||||||
|
|
||||||
val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase())
|
val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase())
|
||||||
|
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags:List<List<String>> = listOf(reportAuthorTag)
|
val tags:List<List<String>> = listOf(reportAuthorTag)
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class RepostEvent (
|
class RepostEvent (
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
|
||||||
@@ -32,10 +33,10 @@ class RepostEvent (
|
|||||||
fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
|
fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
|
||||||
val content = boostedPost.toJson()
|
val content = boostedPost.toJson()
|
||||||
|
|
||||||
val replyToPost = listOf("e", boostedPost.id.toHex())
|
val replyToPost = listOf("e", boostedPost.id)
|
||||||
val replyToAuthor = listOf("p", boostedPost.pubKey.toHex())
|
val replyToAuthor = listOf("p", boostedPost.pubKey)
|
||||||
|
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
|
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
|
||||||
|
|
||||||
if (boostedPost is LongTextNoteEvent) {
|
if (boostedPost is LongTextNoteEvent) {
|
||||||
@@ -44,7 +45,7 @@ class RepostEvent (
|
|||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return RepostEvent(id, pubKey, createdAt, tags, content, sig)
|
return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.Event
|
|
||||||
|
|
||||||
class TextNoteEvent(
|
class TextNoteEvent(
|
||||||
id: ByteArray,
|
id: HexKey,
|
||||||
pubKey: ByteArray,
|
pubKey: HexKey,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: ByteArray
|
sig: HexKey
|
||||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||||
@@ -20,7 +21,7 @@ class TextNoteEvent(
|
|||||||
const val kind = 1
|
const val kind = 1
|
||||||
|
|
||||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
|
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
replyTos?.forEach {
|
replyTos?.forEach {
|
||||||
tags.add(listOf("e", it))
|
tags.add(listOf("e", it))
|
||||||
@@ -33,7 +34,7 @@ class TextNoteEvent(
|
|||||||
}
|
}
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import java.util.UUID
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Nostr Client manages multiple personae the user may switch between. Events are received and
|
* The Nostr Client manages multiple personae the user may switch between. Events are received and
|
||||||
@@ -38,9 +38,7 @@ object Client: RelayPool.Listener {
|
|||||||
if (relays.size != newRelayConfig.size) return false
|
if (relays.size != newRelayConfig.size) return false
|
||||||
|
|
||||||
relays.forEach { oldRelayInfo ->
|
relays.forEach { oldRelayInfo ->
|
||||||
val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url }
|
val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } ?: return false
|
||||||
|
|
||||||
if (newRelayInfo == null) return false
|
|
||||||
|
|
||||||
if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false
|
if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.service.relays
|
|
||||||
|
|
||||||
import fr.acinq.secp256k1.Secp256k1
|
|
||||||
import nostr.postr.events.Event
|
|
||||||
import nostr.postr.events.generateId
|
|
||||||
|
|
||||||
fun Event.hasValidSignature(): Boolean {
|
|
||||||
if (!id.contentEquals(generateId())) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!Secp256k1.get().verifySchnorr(sig, id, pubKey)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.events.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
|
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
@@ -36,8 +37,8 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.font.FontWeight.Companion.W500
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -45,14 +46,14 @@ import androidx.navigation.NavHostController
|
|||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.RoboHashCache
|
import com.vitorpamplona.amethyst.RoboHashCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerContent(navController: NavHostController,
|
fun DrawerContent(navController: NavHostController,
|
||||||
@@ -88,7 +89,8 @@ fun DrawerContent(navController: NavHostController,
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1F),
|
.weight(1F),
|
||||||
accountStateViewModel
|
accountStateViewModel,
|
||||||
|
account,
|
||||||
)
|
)
|
||||||
|
|
||||||
BottomContent(account.userProfile(), scaffoldState, navController)
|
BottomContent(account.userProfile(), scaffoldState, navController)
|
||||||
@@ -155,38 +157,44 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
|
|||||||
if (accountUser.bestDisplayName() != null) {
|
if (accountUser.bestDisplayName() != null) {
|
||||||
Text(
|
Text(
|
||||||
accountUser.bestDisplayName() ?: "",
|
accountUser.bestDisplayName() ?: "",
|
||||||
modifier = Modifier.padding(top = 7.dp).clickable(onClick = {
|
modifier = Modifier
|
||||||
accountUser.let {
|
.padding(top = 7.dp)
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
.clickable(onClick = {
|
||||||
}
|
accountUser.let {
|
||||||
coroutineScope.launch {
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
scaffoldState.drawerState.close()
|
}
|
||||||
}
|
coroutineScope.launch {
|
||||||
}),
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
|
}),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (accountUser.bestUsername() != null) {
|
if (accountUser.bestUsername() != null) {
|
||||||
Text(" @${accountUser.bestUsername()}", color = Color.LightGray,
|
Text(" @${accountUser.bestUsername()}", color = Color.LightGray,
|
||||||
modifier = Modifier.padding(top = 15.dp).clickable(onClick = {
|
modifier = Modifier
|
||||||
accountUser.let {
|
.padding(top = 15.dp)
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
.clickable(onClick = {
|
||||||
}
|
accountUser.let {
|
||||||
coroutineScope.launch {
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
scaffoldState.drawerState.close()
|
}
|
||||||
}
|
coroutineScope.launch {
|
||||||
})
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(modifier = Modifier.padding(top = 15.dp).clickable(onClick = {
|
Row(modifier = Modifier
|
||||||
accountUser.let {
|
.padding(top = 15.dp)
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
.clickable(onClick = {
|
||||||
}
|
accountUser.let {
|
||||||
coroutineScope.launch {
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
scaffoldState.drawerState.close()
|
}
|
||||||
}
|
coroutineScope.launch {
|
||||||
})) {
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
|
})) {
|
||||||
Row() {
|
Row() {
|
||||||
Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold)
|
Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold)
|
||||||
Text(stringResource(R.string.following))
|
Text(stringResource(R.string.following))
|
||||||
@@ -206,66 +214,84 @@ fun ListContent(
|
|||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
scaffoldState: ScaffoldState,
|
scaffoldState: ScaffoldState,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
accountViewModel: AccountStateViewModel
|
accountViewModel: AccountStateViewModel,
|
||||||
|
account: Account,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier.fillMaxHeight()) {
|
||||||
LazyColumn() {
|
if (accountUser != null)
|
||||||
item {
|
NavigationRow(
|
||||||
if (accountUser != null)
|
title = stringResource(R.string.profile),
|
||||||
NavigationRow(navController,
|
icon = Route.Profile.icon,
|
||||||
scaffoldState,
|
tint = MaterialTheme.colors.primary,
|
||||||
"User/${accountUser.pubkeyHex}",
|
navController = navController,
|
||||||
Route.Profile.icon,
|
scaffoldState = scaffoldState,
|
||||||
stringResource(R.string.profile)
|
route = "User/${accountUser.pubkeyHex}",
|
||||||
)
|
)
|
||||||
|
|
||||||
Divider(
|
Divider(thickness = 0.25.dp)
|
||||||
modifier = Modifier.padding(bottom = 15.dp),
|
|
||||||
thickness = 0.25.dp
|
NavigationRow(
|
||||||
)
|
title = stringResource(R.string.security_filters),
|
||||||
Column(modifier = modifier.padding(horizontal = 25.dp)) {
|
icon = Route.Filters.icon,
|
||||||
Row(modifier = Modifier.clickable(onClick = {
|
tint = MaterialTheme.colors.onBackground,
|
||||||
navController.navigate(Route.Filters.route)
|
navController = navController,
|
||||||
coroutineScope.launch {
|
scaffoldState = scaffoldState,
|
||||||
scaffoldState.drawerState.close()
|
route = Route.Filters.route,
|
||||||
}
|
)
|
||||||
})) {
|
|
||||||
Text(
|
Divider(thickness = 0.25.dp)
|
||||||
text = stringResource(R.string.security_filters),
|
|
||||||
fontSize = 18.sp,
|
IconRow(
|
||||||
fontWeight = W500
|
title = "Backup Keys",
|
||||||
)
|
icon = R.drawable.ic_key,
|
||||||
}
|
tint = MaterialTheme.colors.onBackground,
|
||||||
Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) {
|
onClick = { backupDialogOpen = true }
|
||||||
Text(
|
)
|
||||||
text = stringResource(R.string.log_out),
|
|
||||||
modifier = Modifier.padding(vertical = 15.dp),
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = W500
|
IconRow(
|
||||||
)
|
"Logout",
|
||||||
}
|
R.drawable.ic_logout,
|
||||||
}
|
MaterialTheme.colors.onBackground,
|
||||||
}
|
onClick = { accountViewModel.logOff() }
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupDialogOpen) {
|
||||||
|
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: String, icon: Int, title: String) {
|
fun NavigationRow(
|
||||||
|
title: String,
|
||||||
|
icon: Int,
|
||||||
|
tint: Color,
|
||||||
|
navController: NavHostController,
|
||||||
|
scaffoldState: ScaffoldState,
|
||||||
|
route: String,
|
||||||
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val currentRoute = currentRoute(navController)
|
val currentRoute = currentRoute(navController)
|
||||||
|
IconRow(title, icon, tint, onClick = {
|
||||||
|
if (currentRoute != route) {
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
coroutineScope.launch {
|
||||||
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) {
|
||||||
Row(modifier = Modifier
|
Row(modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = {
|
.clickable(onClick = onClick)
|
||||||
if (currentRoute != route) {
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
coroutineScope.launch {
|
|
||||||
scaffoldState.drawerState.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -276,7 +302,7 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(icon), null,
|
painter = painterResource(icon), null,
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(22.dp),
|
||||||
tint = MaterialTheme.colors.primary
|
tint = tint
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ sealed class Route(
|
|||||||
buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}
|
buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}
|
||||||
)
|
)
|
||||||
|
|
||||||
object Filters : Route("Filters", R.drawable.ic_dm,
|
object Filters : Route("Filters", R.drawable.ic_security,
|
||||||
buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }}
|
buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ import com.vitorpamplona.amethyst.RoboHashCache
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
@@ -51,8 +53,9 @@ import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -85,6 +88,7 @@ fun NoteCompose(
|
|||||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val noteEvent = note?.event
|
val noteEvent = note?.event
|
||||||
|
val baseChannel = note?.channel()
|
||||||
|
|
||||||
if (noteEvent == null) {
|
if (noteEvent == null) {
|
||||||
BlankNote(modifier.combinedClickable(
|
BlankNote(modifier.combinedClickable(
|
||||||
@@ -100,6 +104,8 @@ fun NoteCompose(
|
|||||||
navController,
|
navController,
|
||||||
onClick = { showHiddenNote = true }
|
onClick = { showHiddenNote = true }
|
||||||
)
|
)
|
||||||
|
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
||||||
|
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
|
||||||
} else {
|
} else {
|
||||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
@@ -178,7 +184,6 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// boosted picture
|
// boosted picture
|
||||||
val baseChannel = note.channel()
|
|
||||||
if (noteEvent is ChannelMessageEvent && baseChannel != null) {
|
if (noteEvent is ChannelMessageEvent && baseChannel != null) {
|
||||||
val channelState by baseChannel.live.observeAsState()
|
val channelState by baseChannel.live.observeAsState()
|
||||||
val channel = channelState?.channel
|
val channel = channelState?.channel
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Key
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.halilibo.richtext.markdown.Markdown
|
||||||
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
|
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||||
|
import com.halilibo.richtext.ui.resolveDefaults
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import nostr.postr.toNsec
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onClose,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CloseButton(onCancel = onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
MaterialRichText(
|
||||||
|
style = RichTextStyle().resolveDefaults(),
|
||||||
|
) {
|
||||||
|
Markdown(
|
||||||
|
content = stringResource(R.string.account_backup_tips_md),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
|
|
||||||
|
NSecCopyButton(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NSecCopyButton(
|
||||||
|
account: Account
|
||||||
|
) {
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = {
|
||||||
|
account.loggedIn.privKey?.let {
|
||||||
|
clipboardManager.setText(AnnotatedString(it.toNsec()))
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.secret_key_copied_to_clipboard),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
tint = Color.White,
|
||||||
|
imageVector = Icons.Default.Key,
|
||||||
|
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
|
||||||
|
)
|
||||||
|
Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,7 +121,6 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
|
|||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
ChannelHeader(
|
ChannelHeader(
|
||||||
channel, account,
|
channel, account,
|
||||||
accountStateViewModel = accountStateViewModel,
|
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel, navController: NavController) {
|
fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavController) {
|
||||||
val channelState by baseChannel.live.observeAsState()
|
val channelState by baseChannel.live.observeAsState()
|
||||||
val channel = channelState?.channel ?: return
|
val channel = channelState?.channel ?: return
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
|
|||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(accountViewModel) {
|
||||||
NostrChatroomListDataSource.resetFilters()
|
NostrChatroomListDataSource.resetFilters()
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
@@ -99,7 +99,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
|
|||||||
val observer = LifecycleEventObserver { source, event ->
|
val observer = LifecycleEventObserver { source, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
NostrChatroomListDataSource.resetFilters()
|
NostrChatroomListDataSource.resetFilters()
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,22 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) {
|
|||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(accountViewModel) {
|
||||||
NostrChatroomListDataSource.resetFilters()
|
NostrChatroomListDataSource.resetFilters()
|
||||||
feedViewModel.invalidateData() // refresh view
|
feedViewModel.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
DisposableEffect(accountViewModel) {
|
||||||
|
val observer = LifecycleEventObserver { source, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
NostrChatroomListDataSource.resetFilters()
|
||||||
|
feedViewModel.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
|||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
LaunchedEffect(userId) {
|
LaunchedEffect(userId) {
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(userId) {
|
DisposableEffect(userId) {
|
||||||
@@ -87,7 +87,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
|||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Private Message Start")
|
println("Private Message Start")
|
||||||
NostrChatroomDataSource.start()
|
NostrChatroomDataSource.start()
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
println("Private Message Stop")
|
println("Private Message Stop")
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
|
|||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(accountViewModel) {
|
||||||
NostrHomeDataSource.resetFilters()
|
NostrHomeDataSource.resetFilters()
|
||||||
|
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
feedViewModelReplies.invalidateData()
|
feedViewModelReplies.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
@@ -64,8 +64,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
|
|||||||
val observer = LifecycleEventObserver { source, event ->
|
val observer = LifecycleEventObserver { source, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
NostrHomeDataSource.resetFilters()
|
NostrHomeDataSource.resetFilters()
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
feedViewModelReplies.invalidateData()
|
feedViewModelReplies.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon
|
|||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { source, event ->
|
val observer = LifecycleEventObserver { source, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Bolt
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
import androidx.compose.material.icons.filled.EditNote
|
import androidx.compose.material.icons.filled.EditNote
|
||||||
import androidx.compose.material.icons.filled.Key
|
|
||||||
import androidx.compose.material.icons.filled.Link
|
import androidx.compose.material.icons.filled.Link
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
@@ -78,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.toNsec
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -337,10 +335,6 @@ private fun ProfileHeader(
|
|||||||
.padding(bottom = 3.dp)) {
|
.padding(bottom = 3.dp)) {
|
||||||
MessageButton(baseUser, navController)
|
MessageButton(baseUser, navController)
|
||||||
|
|
||||||
if (accountUser == baseUser && account.isWriteable()) {
|
|
||||||
NSecCopyButton(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
NPubCopyButton(baseUser)
|
NPubCopyButton(baseUser)
|
||||||
|
|
||||||
if (accountUser == baseUser) {
|
if (accountUser == baseUser) {
|
||||||
@@ -637,40 +631,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel, navController: Nav
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NSecCopyButton(
|
|
||||||
account: Account
|
|
||||||
) {
|
|
||||||
val clipboardManager = LocalClipboardManager.current
|
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Button(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 3.dp)
|
|
||||||
.width(50.dp),
|
|
||||||
onClick = { popupExpanded = true },
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors = ButtonDefaults
|
|
||||||
.buttonColors(
|
|
||||||
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
tint = Color.White,
|
|
||||||
imageVector = Icons.Default.Key,
|
|
||||||
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = popupExpanded,
|
|
||||||
onDismissRequest = { popupExpanded = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) }; popupExpanded = false }) {
|
|
||||||
Text(stringResource(R.string.copy_private_key_to_the_clipboard))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NPubCopyButton(
|
private fun NPubCopyButton(
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
|
|||||||
val feedViewModel: NostrGlobalFeedViewModel = viewModel()
|
val feedViewModel: NostrGlobalFeedViewModel = viewModel()
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(accountViewModel) {
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
@@ -95,7 +95,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
|
|||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Global Start")
|
println("Global Start")
|
||||||
NostrGlobalDataSource.start()
|
NostrGlobalDataSource.start()
|
||||||
feedViewModel.invalidateData()
|
feedViewModel.refresh()
|
||||||
}
|
}
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
println("Global Stop")
|
println("Global Stop")
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 510 B |
|
After Width: | Height: | Size: 250 B |
|
After Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 443 B |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 662 B |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 991 B |
@@ -24,7 +24,7 @@
|
|||||||
<string name="copy_user_pubkey">Copiar PubKey del usuario</string>
|
<string name="copy_user_pubkey">Copiar PubKey del usuario</string>
|
||||||
<string name="copy_note_id">Copiar ID de la nota</string>
|
<string name="copy_note_id">Copiar ID de la nota</string>
|
||||||
<string name="broadcast">Transmisión</string>
|
<string name="broadcast">Transmisión</string>
|
||||||
<string name="block_hide_user">Bloquear y ocultar usuario></string>
|
<string name="block_hide_user">Bloquear y ocultar usuario</string>
|
||||||
<string name="report_spam_scam">Reportar Spam / Estafa</string>
|
<string name="report_spam_scam">Reportar Spam / Estafa</string>
|
||||||
<string name="report_impersonation">Reportar suplantación de identidad</string>
|
<string name="report_impersonation">Reportar suplantación de identidad</string>
|
||||||
<string name="report_explicit_content">Reportar contenido explícito</string>
|
<string name="report_explicit_content">Reportar contenido explícito</string>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Inicie sesión con una clave privada para poder enviar Zaps</string>
|
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Inicie sesión con una clave privada para poder enviar Zaps</string>
|
||||||
<string name="zaps">Zaps</string>
|
<string name="zaps">Zaps</string>
|
||||||
<string name="view_count">Total vistas</string>
|
<string name="view_count">Total vistas</string>
|
||||||
<string name="boost">Aumentar</string>
|
<string name="boost">Impulsar</string>
|
||||||
<string name="boosted">boosted</string>
|
<string name="boosted">boosted</string>
|
||||||
<string name="quote">Cita</string>
|
<string name="quote">Cita</string>
|
||||||
<string name="new_amount_in_sats">Nueva cantidad en Sats</string>
|
<string name="new_amount_in_sats">Nueva cantidad en Sats</string>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<string name="profile_banner">Banner de perfil</string>
|
<string name="profile_banner">Banner de perfil</string>
|
||||||
<string name="following">" Siguiendo"</string>
|
<string name="following">" Siguiendo"</string>
|
||||||
<string name="followers">" Seguidores"</string>
|
<string name="followers">" Seguidores"</string>
|
||||||
<string name="profile">Peril</string>
|
<string name="profile">Perfil</string>
|
||||||
<string name="security_filters">Filtros de seguridad</string>
|
<string name="security_filters">Filtros de seguridad</string>
|
||||||
<string name="log_out">Cerrar sesión</string>
|
<string name="log_out">Cerrar sesión</string>
|
||||||
<string name="show_more">Mostrar más</string>
|
<string name="show_more">Mostrar más</string>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<string name="description">Descripción</string>
|
<string name="description">Descripción</string>
|
||||||
<string name="about_us">"Sobre nosotros… "</string>
|
<string name="about_us">"Sobre nosotros… "</string>
|
||||||
<string name="what_s_on_your_mind">¿Qué tienes en mente?</string>
|
<string name="what_s_on_your_mind">¿Qué tienes en mente?</string>
|
||||||
<string name="post">Publicación</string>
|
<string name="post">Enviar</string>
|
||||||
<string name="save">Guardar</string>
|
<string name="save">Guardar</string>
|
||||||
<string name="create">Crear</string>
|
<string name="create">Crear</string>
|
||||||
<string name="cancel">Cancelar</string>
|
<string name="cancel">Cancelar</string>
|
||||||
@@ -101,15 +101,15 @@
|
|||||||
<string name="copy_channel_id_note_to_the_clipboard">Copia la nota-ID del canal</string>
|
<string name="copy_channel_id_note_to_the_clipboard">Copia la nota-ID del canal</string>
|
||||||
<string name="edits_the_channel_metadata">Edita los metadatos del canal</string>
|
<string name="edits_the_channel_metadata">Edita los metadatos del canal</string>
|
||||||
<string name="join">Unirse</string>
|
<string name="join">Unirse</string>
|
||||||
<string name="known">Conocido</string>
|
<string name="known">Conocidos</string>
|
||||||
<string name="new_requests">Nuevas solicitudes</string>
|
<string name="new_requests">Nuevas solicitudes</string>
|
||||||
<string name="blocked_users">Usuarios bloqueados</string>
|
<string name="blocked_users">Usuarios bloqueados</string>
|
||||||
<string name="new_threads">Temas nuevos</string>
|
<string name="new_threads">Temas nuevos</string>
|
||||||
<string name="conversations">Conversaciones</string>
|
<string name="conversations">Conversaciones</string>
|
||||||
<string name="notes">Notas</string>
|
<string name="notes">Notas</string>
|
||||||
<string name="replies">Respuestas</string>
|
<string name="replies">Respuestas</string>
|
||||||
<string name="follows">"Sigue"</string>
|
<string name="follows">Siguiendo</string>
|
||||||
<string name="reports">"Reportes"</string>
|
<string name="reports">Reportes</string>
|
||||||
<string name="more_options">Más opciones</string>
|
<string name="more_options">Más opciones</string>
|
||||||
<string name="relays">" Retransmisores"</string>
|
<string name="relays">" Retransmisores"</string>
|
||||||
<string name="website">Sitio web</string>
|
<string name="website">Sitio web</string>
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<string name="key_is_required">Se requiere la clave</string>
|
<string name="key_is_required">Se requiere la clave</string>
|
||||||
<string name="login">Acceso</string>
|
<string name="login">Acceso</string>
|
||||||
<string name="generate_a_new_key">Generar una nueva clave</string>
|
<string name="generate_a_new_key">Generar una nueva clave</string>
|
||||||
<string name="loading_feed">Cargando el tablón</string>
|
<string name="loading_feed">Cargando el tablón…</string>
|
||||||
<string name="error_loading_replies">"Error al cargar las respuestas: "</string>
|
<string name="error_loading_replies">"Error al cargar las respuestas: "</string>
|
||||||
<string name="try_again">Intentar otra vez</string>
|
<string name="try_again">Intentar otra vez</string>
|
||||||
<string name="feed_is_empty">Tablón vacío</string>
|
<string name="feed_is_empty">Tablón vacío</string>
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
<string name="leave">Dejar</string>
|
<string name="leave">Dejar</string>
|
||||||
<string name="unfollow">Dejar de seguir</string>
|
<string name="unfollow">Dejar de seguir</string>
|
||||||
<string name="channel_created">Canal creado</string>
|
<string name="channel_created">Canal creado</string>
|
||||||
<string name="channel_information_changed_to">"La información del canal cambió a"</string>
|
<string name="channel_information_changed_to">La información del canal cambió a</string>
|
||||||
<string name="public_chat">Chat público</string>
|
<string name="public_chat">Chat público</string>
|
||||||
<string name="posts_received">publicaciones recibidas</string>
|
<string name="posts_received">publicaciones recibidas</string>
|
||||||
<string name="remove">Eliminar</string>
|
<string name="remove">Eliminar</string>
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
<string name="to">a</string>
|
<string name="to">a</string>
|
||||||
<string name="show_in">Mostrar en</string>
|
<string name="show_in">Mostrar en</string>
|
||||||
<string name="first">primero</string>
|
<string name="first">primero</string>
|
||||||
<string name="always_translate_to">Traducir siempre a</string>
|
<string name="always_translate_to">"Traducir siempre a "</string>
|
||||||
<string name="nip_05" translatable="false">NIP-05</string>
|
<string name="nip_05" translatable="false">NIP-05</string>
|
||||||
<string name="lnurl" translatable="false">LNURL...</string>
|
<string name="lnurl" translatable="false">LNURL...</string>
|
||||||
<string name="never">nunca</string>
|
<string name="never">nunca</string>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
<string name="website">Website</string>
|
<string name="website">Website</string>
|
||||||
<string name="lightning_address">Lightning Address</string>
|
<string name="lightning_address">Lightning Address</string>
|
||||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
|
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
|
||||||
<string name="copy_private_key_to_the_clipboard">Copy Private Key to the Clipboard</string>
|
<string name="copy_private_key_to_the_clipboard">Copy Secret Key to the Clipboard</string>
|
||||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
|
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
|
||||||
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
|
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
|
||||||
<string name="send_a_direct_message">Send a Direct Message</string>
|
<string name="send_a_direct_message">Send a Direct Message</string>
|
||||||
@@ -173,4 +173,12 @@
|
|||||||
<string name="report_hateful_speech">Report Hateful speech</string>
|
<string name="report_hateful_speech">Report Hateful speech</string>
|
||||||
<string name="report_nudity_porn">Report Nudity / Porn</string>
|
<string name="report_nudity_porn">Report Nudity / Porn</string>
|
||||||
<string name="others">others</string>
|
<string name="others">others</string>
|
||||||
|
<string name="account_backup_tips_md">
|
||||||
|
## Key Backup and Safety Tips
|
||||||
|
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
|
||||||
|
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
||||||
|
\n- Amethyst developers will **never** ask you for your secret key.
|
||||||
|
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
|
||||||
|
</string>
|
||||||
|
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class Nip19Test {
|
||||||
|
|
||||||
|
private val nip19 = Nip19();
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun to_int_32_length_smaller_than_4() {
|
||||||
|
toInt32(byteArrayOfInts(1, 2, 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun to_int_32_length_bigger_than_4() {
|
||||||
|
toInt32(byteArrayOfInts(1, 2, 3, 4, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun to_int_32_length_4() {
|
||||||
|
val actual = toInt32(byteArrayOfInts(1, 2, 3, 4))
|
||||||
|
|
||||||
|
Assert.assertEquals(16909060, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Test not implemented yet")
|
||||||
|
@Test()
|
||||||
|
fun parse_TLV() {
|
||||||
|
// TODO: I don't know how to test this (?)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_null() {
|
||||||
|
val actual = nip19.uriToRoute(null)
|
||||||
|
|
||||||
|
Assert.assertEquals(null, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_unknown() {
|
||||||
|
val actual = nip19.uriToRoute("nostr:unknown")
|
||||||
|
|
||||||
|
Assert.assertEquals(null, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_npub() {
|
||||||
|
val actual =
|
||||||
|
nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y")
|
||||||
|
|
||||||
|
Assert.assertEquals(Nip19.Type.USER, actual?.type)
|
||||||
|
Assert.assertEquals(
|
||||||
|
"bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365",
|
||||||
|
actual?.hex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_note() {
|
||||||
|
val actual =
|
||||||
|
nip19.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")
|
||||||
|
|
||||||
|
Assert.assertEquals(Nip19.Type.NOTE, actual?.type)
|
||||||
|
Assert.assertEquals(
|
||||||
|
"82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3",
|
||||||
|
actual?.hex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Test not implemented yet")
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_nprofile() {
|
||||||
|
val actual = nip19.uriToRoute("nostr:nprofile")
|
||||||
|
|
||||||
|
Assert.assertEquals(Nip19.Type.USER, actual?.type)
|
||||||
|
Assert.assertEquals("*", actual?.hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Test not implemented yet")
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_nevent() {
|
||||||
|
val actual = nip19.uriToRoute("nostr:nevent")
|
||||||
|
|
||||||
|
Assert.assertEquals(Nip19.Type.USER, actual?.type)
|
||||||
|
Assert.assertEquals("*", actual?.hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Test not implemented yet")
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_nrelay() {
|
||||||
|
val actual = nip19.uriToRoute("nostr:nrelay")
|
||||||
|
|
||||||
|
Assert.assertEquals(Nip19.Type.RELAY, actual?.type)
|
||||||
|
Assert.assertEquals("*", actual?.hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Test not implemented yet")
|
||||||
|
@Test()
|
||||||
|
fun uri_to_route_naddr() {
|
||||||
|
val actual = nip19.uriToRoute("nostr:naddr")
|
||||||
|
|
||||||
|
Assert.assertEquals(Nip19.Type.ADDRESS, actual?.type)
|
||||||
|
Assert.assertEquals("*", actual?.hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
|
||||||
|
}
|
||||||