Format last files with conflicts

This commit is contained in:
Chemaclass
2023-03-07 20:43:34 +01:00
parent f1b6927bb3
commit c9b859610e
14 changed files with 1673 additions and 1697 deletions
File diff suppressed because it is too large Load Diff
@@ -6,13 +6,6 @@ import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -20,11 +13,17 @@ 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 java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
class AddressableNote(val address: ATag) : Note(address.toTag()) {
class AddressableNote(val address: ATag): Note(address.toTag()) {
override fun idNote() = address.toNAddr() override fun idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex() override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address override fun address() = address
@@ -62,9 +61,9 @@ open class Note(val idHex: String) {
fun channel(): Channel? { fun channel(): Channel? {
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 } ?: (event as? ChannelCreateEvent)?.let { it.id }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
} }
@@ -137,7 +136,7 @@ open class Note(val idHex: String) {
fun removeReport(deleteNote: Note) { fun removeReport(deleteNote: Note) {
val author = deleteNote.author ?: return val author = deleteNote.author ?: return
if (author in reports.keys && reports[author]?.contains(deleteNote) == true ) { if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
reports[author]?.let { reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote)) reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.reports?.invalidateData() liveSet?.reports?.invalidateData()
@@ -156,7 +155,6 @@ open class Note(val idHex: String) {
} }
} }
fun addBoost(note: Note) { fun addBoost(note: Note) {
if (note !in boosts) { if (note !in boosts) {
boosts = boosts + note boosts = boosts + note
@@ -240,11 +238,13 @@ open class Note(val idHex: String) {
} }
fun hasAnyReports(): Boolean { fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24*60*60 val dayAgo = Date().time / 1000 - 24 * 60 * 60
return reports.isNotEmpty() || return reports.isNotEmpty() ||
(author?.reports?.values?.filter { (
it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null author?.reports?.values?.filter {
}?.isNotEmpty() ?: false) it.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null
}?.isNotEmpty() ?: false
)
} }
fun directlyCiteUsersHex(): Set<HexKey> { fun directlyCiteUsersHex(): Set<HexKey> {
@@ -257,7 +257,6 @@ open class Note(val idHex: String) {
returningList.add(tag[1]) returningList.add(tag[1])
} }
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
return returningList return returningList
@@ -275,17 +274,16 @@ open class Note(val idHex: String) {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
return returningList return returningList
} }
fun directlyCites(userProfile: User): Boolean { fun directlyCites(userProfile: User): Boolean {
return author == userProfile return author == userProfile ||
|| (userProfile in directlyCiteUsers()) (userProfile in directlyCiteUsers()) ||
|| (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) ||
|| (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
} }
fun isNewThread(): Boolean { fun isNewThread(): Boolean {
@@ -306,7 +304,7 @@ open class Note(val idHex: String) {
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val currentTime = Date().time / 1000 val currentTime = Date().time / 1000
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection
} }
fun boostedBy(loggedIn: User): List<Note> { fun boostedBy(loggedIn: User): List<Note> {
@@ -329,7 +327,6 @@ open class Note(val idHex: String) {
} }
} }
class NoteLiveSet(u: Note) { class NoteLiveSet(u: Note) {
// Observers line up here. // Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u) val metadata: NoteLiveData = NoteLiveData(u)
@@ -342,17 +339,17 @@ class NoteLiveSet(u: Note) {
val zaps: NoteLiveData = NoteLiveData(u) val zaps: NoteLiveData = NoteLiveData(u)
fun isInUse(): Boolean { fun isInUse(): Boolean {
return metadata.hasObservers() return metadata.hasObservers() ||
|| reactions.hasObservers() reactions.hasObservers() ||
|| boosts.hasObservers() boosts.hasObservers() ||
|| replies.hasObservers() replies.hasObservers() ||
|| reports.hasObservers() reports.hasObservers() ||
|| relays.hasObservers() relays.hasObservers() ||
|| zaps.hasObservers() zaps.hasObservers()
} }
} }
class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) { class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches. // Refreshes observers in batches.
var handlerWaiting = AtomicBoolean() var handlerWaiting = AtomicBoolean()
@@ -384,7 +381,6 @@ class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
} else { } else {
NostrSingleEventDataSource.add(note) NostrSingleEventDataSource.add(note)
} }
} }
override fun onInactive() { override fun onInactive() {
@@ -6,71 +6,72 @@ import kotlin.time.measureTimedValue
class ThreadAssembler { class ThreadAssembler {
fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? { fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
testedNotes.add(note) testedNotes.add(note)
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot) if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot)
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
if (hasNoReplyTo != null) return hasNoReplyTo if (hasNoReplyTo != null) return hasNoReplyTo
// recursive // recursive
val roots = note.replyTo?.map { val roots = note.replyTo?.map {
if (it !in testedNotes) if (it !in testedNotes) {
searchRoot(it, testedNotes) searchRoot(it, testedNotes)
else } else {
null null
}?.filterNotNull() }
}?.filterNotNull()
if (roots != null && roots.isNotEmpty()) { if (roots != null && roots.isNotEmpty()) {
return roots[0] return roots[0]
}
return null
} }
return null @OptIn(ExperimentalTime::class)
} fun findThreadFor(noteId: String): Set<Note> {
val (result, elapsed) = measureTimedValue {
val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId, null)
if (aTag != null) {
LocalCache.getOrCreateAddressableNote(aTag)
} else {
return emptySet()
}
} else {
LocalCache.getOrCreateNote(noteId)
}
@OptIn(ExperimentalTime::class) if (note.event != null) {
fun findThreadFor(noteId: String): Set<Note> { val thread = mutableSetOf<Note>()
val (result, elapsed) = measureTimedValue {
val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId, null)
if (aTag != null)
LocalCache.getOrCreateAddressableNote(aTag)
else
return emptySet()
} else {
LocalCache.getOrCreateNote(noteId)
}
val threadRoot = searchRoot(note, thread) ?: note
if (note.event != null) { loadDown(threadRoot, thread)
val thread = mutableSetOf<Note>()
val threadRoot = searchRoot(note, thread) ?: note thread.toSet()
} else {
setOf(note)
}
}
loadDown(threadRoot, thread) println("Model Refresh: Thread loaded in $elapsed")
thread.toSet() return result
} else {
setOf(note)
}
} }
println("Model Refresh: Thread loaded in ${elapsed}") fun loadDown(note: Note, thread: MutableSet<Note>) {
if (note !in thread) {
thread.add(note)
return result note.replies.forEach {
} loadDown(it, thread)
}
fun loadDown(note: Note, thread: MutableSet<Note>) { }
if (note !in thread) {
thread.add(note)
note.replies.forEach {
loadDown(it, thread)
}
} }
}
} }
@@ -7,132 +7,132 @@ import java.nio.ByteOrder
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, val relay: String?)
fun uriToRoute(uri: String?): Return? {
try {
val key = uri?.removePrefix("nostr:") ?: return null
val bytes = key.bechToBytes()
if (key.startsWith("npub")) {
return npub(bytes)
} else if (key.startsWith("note")) {
return note(bytes)
} else if (key.startsWith("nprofile")) {
return nprofile(bytes)
} else if (key.startsWith("nevent")) {
return nevent(bytes)
} else if (key.startsWith("nrelay")) {
return nrelay(bytes)
} else if (key.startsWith("naddr")) {
return naddr(bytes)
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
} }
return null data class Return(val type: Type, val hex: String, val relay: String?)
}
private fun npub(bytes: ByteArray): Return { fun uriToRoute(uri: String?): Return? {
return Return(Type.USER, bytes.toHexKey(), null) try {
} val key = uri?.removePrefix("nostr:") ?: return null
private fun note(bytes: ByteArray): Return { val bytes = key.bechToBytes()
return Return(Type.NOTE, bytes.toHexKey(), null); if (key.startsWith("npub")) {
} return npub(bytes)
} else if (key.startsWith("note")) {
return note(bytes)
} else if (key.startsWith("nprofile")) {
return nprofile(bytes)
} else if (key.startsWith("nevent")) {
return nevent(bytes)
} else if (key.startsWith("nrelay")) {
return nrelay(bytes)
} else if (key.startsWith("naddr")) {
return naddr(bytes)
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 $uri: ${e.message}")
}
private fun nprofile(bytes: ByteArray): Return? { return null
val tlv = parseTLV(bytes) }
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id) private fun npub(bytes: ByteArray): Return {
?.get(0) return Return(Type.USER, bytes.toHexKey(), null)
?.toHexKey() ?: return null }
val relay = tlv.get(NIP19TLVTypes.RELAY.id) private fun note(bytes: ByteArray): Return {
?.get(0) return Return(Type.NOTE, bytes.toHexKey(), null)
?.toString(Charsets.UTF_8) }
return Return(Type.USER, hex, relay) private fun nprofile(bytes: ByteArray): Return? {
} val tlv = parseTLV(bytes)
private fun nevent(bytes: ByteArray): Return? { val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
val tlv = parseTLV(bytes) ?.get(0)
?.toHexKey() ?: return null
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id) val relay = tlv.get(NIP19TLVTypes.RELAY.id)
?.get(0) ?.get(0)
?.toHexKey() ?: return null ?.toString(Charsets.UTF_8)
val relay = tlv.get(NIP19TLVTypes.RELAY.id) return Return(Type.USER, hex, relay)
?.get(0) }
?.toString(Charsets.UTF_8)
return Return(Type.USER, hex, relay) private fun nevent(bytes: ByteArray): Return? {
} val tlv = parseTLV(bytes)
private fun nrelay(bytes: ByteArray): Return? { val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
val relayUrl = parseTLV(bytes) ?.get(0)
.get(NIP19TLVTypes.SPECIAL.id) ?.toHexKey() ?: return null
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl, null) val relay = tlv.get(NIP19TLVTypes.RELAY.id)
} ?.get(0)
?.toString(Charsets.UTF_8)
private fun naddr(bytes: ByteArray): Return? { return Return(Type.USER, hex, relay)
val tlv = parseTLV(bytes) }
val d = tlv.get(NIP19TLVTypes.SPECIAL.id) private fun nrelay(bytes: ByteArray): Return? {
?.get(0) val relayUrl = parseTLV(bytes)
?.toString(Charsets.UTF_8) ?: return null .get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
val relay = tlv.get(NIP19TLVTypes.RELAY.id) return Return(Type.RELAY, relayUrl, null)
?.get(0) }
?.toString(Charsets.UTF_8)
val author = tlv.get(NIP19TLVTypes.AUTHOR.id) private fun naddr(bytes: ByteArray): Return? {
?.get(0) val tlv = parseTLV(bytes)
?.toHexKey()
val kind = tlv.get(NIP19TLVTypes.KIND.id) val d = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0) ?.get(0)
?.let { toInt32(it) } ?.toString(Charsets.UTF_8) ?: return null
return Return(Type.ADDRESS, "$kind:$author:$d", relay) 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", relay)
}
} }
// Classes should start with an uppercase letter in kotlin // Classes should start with an uppercase letter in kotlin
enum class NIP19TLVTypes(val id: Byte) { enum class NIP19TLVTypes(val id: Byte) {
SPECIAL(0), SPECIAL(0),
RELAY(1), RELAY(1),
AUTHOR(2), AUTHOR(2),
KIND(3); KIND(3);
} }
fun toInt32(bytes: ByteArray): Int { fun toInt32(bytes: ByteArray): Int {
require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
} }
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> { fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
val 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[t] = mutableListOf() result[t] = mutableListOf()
}
result[t]?.add(v)
} }
result[t]?.add(v) return result
}
return result
} }
@@ -4,114 +4,114 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String?) { fun loadUserProfile(userId: String?) {
if (userId != null) { if (userId != null) {
user = LocalCache.getOrCreateUser(userId) user = LocalCache.getOrCreateUser(userId)
} else { } else {
user = null user = null
}
resetFilters()
} }
resetFilters() fun createUserInfoFilter() = user?.let {
} TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(it.pubkeyHex),
limit = 1
)
)
}
fun createUserInfoFilter() = user?.let { fun createUserPostsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(MetadataEvent.kind), kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
authors = listOf(it.pubkeyHex), authors = listOf(it.pubkeyHex),
limit = 1 limit = 200
) )
) )
} }
fun createUserPostsFilter() = user?.let { fun createUserReceivedZapsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), kinds = listOf(LnZapEvent.kind),
authors = listOf(it.pubkeyHex), tags = mapOf("p" to listOf(it.pubkeyHex))
limit = 200 )
) )
) }
}
fun createUserReceivedZapsFilter() = user?.let { fun createFollowFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(LnZapEvent.kind), kinds = listOf(ContactListEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)) authors = listOf(it.pubkeyHex),
) limit = 1
) )
} )
}
fun createFollowFilter() = user?.let { fun createFollowersFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(ContactListEvent.kind), kinds = listOf(ContactListEvent.kind),
authors = listOf(it.pubkeyHex), tags = mapOf("p" to listOf(it.pubkeyHex))
limit = 1 )
) )
) }
}
fun createFollowersFilter() = user?.let { fun createAcceptedAwardsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(ContactListEvent.kind), kinds = listOf(BadgeProfilesEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)) authors = listOf(it.pubkeyHex),
) limit = 1
) )
} )
}
fun createAcceptedAwardsFilter() = user?.let { fun createReceivedAwardsFilter() = user?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind), kinds = listOf(BadgeAwardEvent.kind),
authors = listOf(it.pubkeyHex), tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 1 limit = 20
) )
) )
} }
fun createReceivedAwardsFilter() = user?.let { val userInfoChannel = requestNewChannel()
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(BadgeAwardEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 20
)
)
}
val userInfoChannel = requestNewChannel() override fun updateChannelFilters() {
userInfoChannel.typedFilters = listOfNotNull(
override fun updateChannelFilters() { createUserInfoFilter(),
userInfoChannel.typedFilters = listOfNotNull( createUserPostsFilter(),
createUserInfoFilter(), createFollowFilter(),
createUserPostsFilter(), createFollowersFilter(),
createFollowFilter(), createUserReceivedZapsFilter(),
createFollowersFilter(), createAcceptedAwardsFilter(),
createUserReceivedZapsFilter(), createReceivedAwardsFilter()
createAcceptedAwardsFilter(), ).ifEmpty { null }
createReceivedAwardsFilter() }
).ifEmpty { null } }
}
}
@@ -23,8 +23,9 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
var fullArray = var fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag
if (relay != null) if (relay != null) {
fullArray = fullArray + byteArrayOf(NIP19TLVTypes.RELAY.id, relay.size.toByte()) + relay fullArray = fullArray + byteArrayOf(NIP19TLVTypes.RELAY.id, relay.size.toByte()) + relay
}
fullArray = fullArray + fullArray = fullArray +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, author.size.toByte()) + author + byteArrayOf(NIP19TLVTypes.AUTHOR.id, author.size.toByte()) + author +
@@ -39,19 +40,20 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
} }
fun parse(address: String, relay: String?): ATag? { fun parse(address: String, relay: String?): ATag? {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) {
parseNAddr(address) parseNAddr(address)
else } else {
parseAtag(address, relay) parseAtag(address, relay)
}
} }
fun parseAtag(atag: String, relay: String?): ATag? { fun parseAtag(atag: String, relay: String?): ATag? {
return try { return try {
val parts = atag.split(":") val parts = atag.split(":")
Hex.decode(parts[1]) Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2], relay) ATag(parts[0].toInt(), parts[1], parts[2], relay)
} catch (t: Throwable) { } catch (t: Throwable) {
Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}") Log.w("ATag", "Error parsing A Tag: $atag: ${t.message}")
null null
} }
} }
@@ -67,16 +69,16 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
if (kind != null && author != null) if (kind != null && author != null) {
return ATag(kind, author, d, relay) return ATag(kind, author, d, relay)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}") Log.w("ATag", "Issue trying to Decode NIP19 $this: ${e.message}")
//e.printStackTrace() // e.printStackTrace()
} }
return null return null
} }
} }
} }
@@ -6,57 +6,57 @@ import com.vitorpamplona.amethyst.service.relays.Client
import java.math.BigDecimal import java.math.BigDecimal
class LnZapEvent( class LnZapEvent(
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
): LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) {
override fun zappedPost() = tags override fun zappedPost() = tags
.filter { it.firstOrNull() == "e" } .filter { it.firstOrNull() == "e" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
override fun zappedAuthor() = tags override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" } .filter { it.firstOrNull() == "p" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
override fun taggedAddresses(): List<ATag> = tags override fun taggedAddresses(): List<ATag> = tags
.filter { it.firstOrNull() == "a" } .filter { it.firstOrNull() == "a" }
.mapNotNull { .mapNotNull {
val aTagValue = it.getOrNull(1) val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2) val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
} }
override fun amount(): BigDecimal? { // Keeps this as a field because it's a heavier function used everywhere.
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
}
// Keeps this as a field because it's a heavier function used everywhere. override fun containedPost(): Event? = try {
val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } description()?.let {
fromJson(it, Client.lenient)
override fun containedPost(): Event? = try { }
description()?.let { } catch (e: Exception) {
fromJson(it, Client.lenient) null
} }
} catch (e: Exception) {
null
}
private fun lnInvoice(): String? = tags private fun lnInvoice(): String? = tags
.filter { it.firstOrNull() == "bolt11" } .filter { it.firstOrNull() == "bolt11" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
.firstOrNull() .firstOrNull()
private fun description(): String? = tags private fun description(): String? = tags
.filter { it.firstOrNull() == "description" } .filter { it.firstOrNull() == "description" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
.firstOrNull() .firstOrNull()
companion object { companion object {
const val kind = 9735 const val kind = 9735
} }
} }
@@ -2,59 +2,58 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.toHex import java.util.Date
class LnZapRequestEvent ( class LnZapRequestEvent(
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey 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) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1) val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2) val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 9734
fun create(originalNote: EventInterface, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
listOf("relays") + relays
)
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
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 { companion object {
val content = "" const val kind = 9734
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf( fun create(originalNote: EventInterface, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
listOf("p", userHex), val content = ""
listOf("relays") + relays val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
) var tags = listOf(
val id = generateId(pubKey, createdAt, kind, tags, content) listOf("e", originalNote.id()),
val sig = Utils.sign(id, privateKey) listOf("p", originalNote.pubKey()),
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) listOf("relays") + relays
)
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
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 {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
} }
}
} }
/* /*
@@ -2,50 +2,49 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.toHex import java.util.Date
class ReactionEvent ( class ReactionEvent(
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey 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) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1) val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2) val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 7
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
return create("\u26A0\uFE0F", originalNote, privateKey, createdAt)
} }
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { companion object {
return create("+", originalNote, privateKey, createdAt) const val kind = 7
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
return create("\u26A0\uFE0F", originalNote, privateKey, createdAt)
}
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
return create("+", originalNote, privateKey, createdAt)
}
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
} }
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
} }
@@ -2,99 +2,98 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.toHex import java.util.Date
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: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey 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 {
// Works with old and new structures for report. // Works with old and new structures for report.
var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
if (reportType == null) { if (reportType == null) {
reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
} }
if (reportType == null) { if (reportType == null) {
reportType = ReportType.SPAM reportType = ReportType.SPAM
} }
return reportType return reportType
}
fun reportedPost() = tags
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
)
} }
fun reportedAuthor() = tags fun reportedPost() = tags
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
.map { .map {
ReportedKey( ReportedKey(
it[1], it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType() it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType()
) )
}
fun reportedAuthor() = tags
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType()
)
}
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
} }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { companion object {
val aTagValue = it.getOrNull(1) const val kind = 1984
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
} val content = ""
companion object { val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
const val kind = 1984 val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val content = "" var tags: List<List<String>> = listOf(reportPostTag, reportAuthorTag)
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) if (reportedPost is LongTextNoteEvent) {
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase()) tags = tags + listOf(listOf("a", reportedPost.address().toTag()))
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val id = generateId(pubKey, createdAt, kind, tags, content)
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag) val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
if (reportedPost is LongTextNoteEvent) { fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
tags = tags + listOf( listOf("a", reportedPost.address().toTag()) ) val content = ""
}
val id = generateId(pubKey, createdAt, kind, tags, content) val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
} }
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { enum class ReportType() {
val content = "" EXPLICIT, // Not used anymore.
ILLEGAL,
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase()) SPAM,
IMPERSONATION,
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() NUDITY,
val tags:List<List<String>> = listOf(reportAuthorTag) PROFANITY
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
}
enum class ReportType() {
EXPLICIT, // Not used anymore.
ILLEGAL,
SPAM,
IMPERSONATION,
NUDITY,
PROFANITY,
}
} }
@@ -3,53 +3,52 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey 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 nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
class RepostEvent ( class RepostEvent(
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } if (aTagValue != null) ATag.parse(aTagValue, relay) else null
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1) fun containedPost() = try {
val relay = it.getOrNull(2) fromJson(content, Client.lenient)
} catch (e: Exception) {
if (aTagValue != null) ATag.parse(aTagValue, relay) else null null
} }
fun containedPost() = try { companion object {
fromJson(content, Client.lenient) const val kind = 6
} catch (e: Exception) {
null fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
} val content = boostedPost.toJson()
companion object { val replyToPost = listOf("e", boostedPost.id())
const val kind = 6 val replyToAuthor = listOf("p", boostedPost.pubKey())
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val content = boostedPost.toJson() var tags: List<List<String>> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor))
val replyToPost = listOf("e", boostedPost.id()) if (boostedPost is LongTextNoteEvent) {
val replyToAuthor = listOf("p", boostedPost.pubKey()) tags = tags + listOf(listOf("a", boostedPost.address().toTag()))
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags:List<List<String>> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor)) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
if (boostedPost is LongTextNoteEvent) { return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
tags = tags + listOf( listOf("a", boostedPost.address().toTag()) ) }
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
}
} }
@@ -15,67 +15,67 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
@Composable @Composable
fun ClickableRoute( fun ClickableRoute(
nip19: Nip19.Return, nip19: Nip19.Return,
navController: NavController navController: NavController
) { ) {
if (nip19.type == Nip19.Type.USER) { if (nip19.type == Nip19.Type.USER) {
val userBase = LocalCache.getOrCreateUser(nip19.hex) val userBase = LocalCache.getOrCreateUser(nip19.hex)
val userState by userBase.live().metadata.observeAsState() val userState by userBase.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val route = "User/${nip19.hex}" val route = "User/${nip19.hex}"
val text = user.toBestDisplayName() val text = user.toBestDisplayName()
ClickableText( ClickableText(
text = AnnotatedString("@${text} "), text = AnnotatedString("@$text "),
onClick = { navController.navigate(route) }, onClick = { navController.navigate(route) },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
) )
} else if (nip19.type == Nip19.Type.ADDRESS) { } else if (nip19.type == Nip19.Type.ADDRESS) {
val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex) val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
if (noteBase == null) { if (noteBase == null) {
Text( Text(
"@${nip19.hex} " "@${nip19.hex} "
) )
} else {
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else if (nip19.type == Nip19.Type.NOTE) {
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
if (note.event is ChannelCreateEvent) {
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Channel/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else if (note.channel() != null) {
ClickableText(
text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel()?.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else { } else {
val noteState by noteBase.live().metadata.observeAsState() Text(
val note = noteState?.note ?: return "@${nip19.hex} "
)
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} }
} else if (nip19.type == Nip19.Type.NOTE) { }
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
if (note.event is ChannelCreateEvent) {
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Channel/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else if (note.channel() != null) {
ClickableText(
text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "),
onClick = { navController.navigate("Channel/${note.channel()?.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else {
Text(
"@${nip19.hex} "
)
}
}
@@ -8,28 +8,20 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -41,9 +33,9 @@ import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.service.Nip19
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.net.MalformedURLException import java.net.MalformedURLException
@@ -61,240 +53,236 @@ val hashTagsPattern: Pattern = Pattern.compile("#([A-Za-z0-9_-]+)")
val urlPattern: Pattern = Patterns.WEB_URL val urlPattern: Pattern = Patterns.WEB_URL
fun isValidURL(url: String?): Boolean { fun isValidURL(url: String?): Boolean {
return try { return try {
URL(url).toURI() URL(url).toURI()
true true
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
false false
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
false false
} }
} }
@Composable @Composable
fun RichTextViewer( fun RichTextViewer(
content: String, content: String,
canPreview: Boolean, canPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tags: List<List<String>>?, tags: List<List<String>>?,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController, navController: NavController
) { ) {
val myMarkDownStyle = RichTextStyle().resolveDefaults().copy(
val myMarkDownStyle = RichTextStyle().resolveDefaults().copy( codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy( textStyle = TextStyle(
textStyle = TextStyle( fontFamily = FontFamily.Monospace,
fontFamily = FontFamily.Monospace, fontSize = 14.sp
fontSize = 14.sp ),
), modifier = Modifier
modifier = Modifier .padding(0.dp)
.padding(0.dp) .fillMaxWidth()
.fillMaxWidth() .clip(shape = RoundedCornerShape(15.dp))
.clip(shape = RoundedCornerShape(15.dp)) .border(
.border( 1.dp,
1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp)
RoundedCornerShape(15.dp) )
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor))
),
stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
),
codeStyle = SpanStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
)
) )
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor))
),
stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
),
codeStyle = SpanStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
)
) )
)
Column(modifier = modifier.animateContentSize()) { Column(modifier = modifier.animateContentSize()) {
if (content.startsWith("# ") ||
if ( content.startsWith("# ") content.contains("##") ||
|| content.contains("##") content.contains("**") ||
|| content.contains("**") content.contains("__") ||
|| content.contains("__") content.contains("```")
|| content.contains("```") ) {
) { MaterialRichText(
style = myMarkDownStyle
MaterialRichText( ) {
style = myMarkDownStyle, Markdown(
) { content = content,
Markdown( markdownParseOptions = MarkdownParseOptions.Default
content = content,
markdownParseOptions = MarkdownParseOptions.Default,
)
}
} else {
// FlowRow doesn't work well with paragraphs. So we need to split them
content.split('\n').forEach { paragraph ->
FlowRow() {
val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ');
s.forEach { word: String ->
if (canPreview) {
// Explicit URL
val lnInvoice = LnInvoiceUtil.findInvoice(word)
if (lnInvoice != null) {
InvoicePreview(lnInvoice)
} else if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
ZoomableImageView(word)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
VideoView(word)
} else {
UrlPreview(word, word)
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) {
UrlPreview("https://$word", word)
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (isBechLink(word)) {
BechLink(word, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
) )
}
} else {
if (isValidURL(word)) {
ClickableUrl("$word ", word)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) {
ClickableUrl(word, "https://$word")
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (isBechLink(word)) {
BechLink(word, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}
} }
} } else {
// FlowRow doesn't work well with paragraphs. So we need to split them
content.split('\n').forEach { paragraph ->
FlowRow() {
val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ')
s.forEach { word: String ->
if (canPreview) {
// Explicit URL
val lnInvoice = LnInvoiceUtil.findInvoice(word)
if (lnInvoice != null) {
InvoicePreview(lnInvoice)
} else if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
ZoomableImageView(word)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
VideoView(word)
} else {
UrlPreview(word, word)
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) {
UrlPreview("https://$word", word)
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (isBechLink(word)) {
BechLink(word, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else {
if (isValidURL(word)) {
ClickableUrl("$word ", word)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (noProtocolUrlValidator.matcher(word).matches()) {
ClickableUrl(word, "https://$word")
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (isBechLink(word)) {
BechLink(word, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
}
}
}
} }
}
} }
}
} }
private fun isArabic(text: String): Boolean { private fun isArabic(text: String): Boolean {
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
} }
fun isBechLink(word: String): Boolean { fun isBechLink(word: String): Boolean {
return word.startsWith("nostr:", true) return word.startsWith("nostr:", true) ||
|| word.startsWith("npub1", true) word.startsWith("npub1", true) ||
|| word.startsWith("naddr1", true) word.startsWith("naddr1", true) ||
|| word.startsWith("note1", true) word.startsWith("note1", true) ||
|| word.startsWith("nprofile1", true) word.startsWith("nprofile1", true) ||
|| word.startsWith("nevent1", true) word.startsWith("nevent1", true) ||
|| word.startsWith("@npub1", true) word.startsWith("@npub1", true) ||
|| word.startsWith("@note1", true) word.startsWith("@note1", true) ||
|| word.startsWith("@addr1", true) word.startsWith("@addr1", true) ||
|| word.startsWith("@nprofile1", true) word.startsWith("@nprofile1", true) ||
|| word.startsWith("@nevent1", true) word.startsWith("@nevent1", true)
} }
@Composable @Composable
fun BechLink(word: String, navController: NavController) { fun BechLink(word: String, navController: NavController) {
val uri = if (word.startsWith("nostr", true)) { val uri = if (word.startsWith("nostr", true)) {
word word
} else if (word.startsWith("@")) { } else if (word.startsWith("@")) {
word.replaceFirst("@", "nostr:") word.replaceFirst("@", "nostr:")
} else { } else {
"nostr:${word}" "nostr:$word"
} }
val nip19Route = try { val nip19Route = try {
Nip19().uriToRoute(uri) Nip19().uriToRoute(uri)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
if (nip19Route == null) { if (nip19Route == null) {
Text(text = "$word ") Text(text = "$word ")
} else { } else {
ClickableRoute(nip19Route, navController) ClickableRoute(nip19Route, navController)
} }
} }
@Composable @Composable
fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) {
val matcher = tagIndex.matcher(word) val matcher = tagIndex.matcher(word)
val index = try { val index = try {
matcher.find() matcher.find()
matcher.group(1).toInt() matcher.group(1).toInt()
} catch (e: Exception) { } catch (e: Exception) {
println("Couldn't link tag ${word}") println("Couldn't link tag $word")
null null
} }
if (index == null) { if (index == null) {
return Text(text = "$word ") return Text(text = "$word ")
} }
if (index >= 0 && index < tags.size) { if (index >= 0 && index < tags.size) {
if (tags[index][0] == "p") { if (tags[index][0] == "p") {
val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1]) val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1])
if (baseUser != null) { if (baseUser != null) {
val userState = baseUser.live().metadata.observeAsState() val userState = baseUser.live().metadata.observeAsState()
val user = userState.value?.user val user = userState.value?.user
if (user != null) { if (user != null) {
ClickableUserTag(user, navController) ClickableUserTag(user, navController)
} else {
Text(text = "$word ")
}
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else if (tags[index][0] == "e") {
val note = LocalCache.checkGetOrCreateNote(tags[index][1])
if (note != null) {
if (canPreview) {
NoteCompose(
baseNote = note,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
),
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
.compositeOver(backgroundColor),
isQuotedNote = true,
navController = navController
)
} else {
ClickableNoteTag(note, navController)
}
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else { } else {
Text(text = "$word ") Text(text = "$word ")
} }
} else { }
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else if (tags[index][0] == "e") {
val note = LocalCache.checkGetOrCreateNote(tags[index][1])
if (note != null) {
if (canPreview) {
NoteCompose(
baseNote = note,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
),
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
.compositeOver(backgroundColor),
isQuotedNote = true,
navController = navController
)
} else {
ClickableNoteTag(note, navController)
}
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
}
} else
Text(text = "$word ")
}
} }
@@ -6,51 +6,51 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class NIP19ParserTest { class NIP19ParserTest {
@Test @Test
fun nAddrParser() { fun nAddrParser() {
val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus") val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus")
assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex) assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex)
} }
@Test @Test
fun nAddrParser2() { fun nAddrParser2() {
val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8") val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8")
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex) assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
} }
@Test @Test
fun nAddrParse3() { fun nAddrParse3() {
val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38") val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38")
assertEquals(Nip19.Type.ADDRESS, result?.type) assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex) assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay) assertEquals("wss://relay.damus.io", result?.relay)
} }
@Test @Test
fun nAddrATagParse3() { fun nAddrATagParse3() {
val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io") val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io")
assertEquals(30023, address?.kind) assertEquals(30023, address?.kind)
assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex) assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex)
assertEquals("89de7920", address?.dTag) assertEquals("89de7920", address?.dTag)
assertEquals("wss://relay.damus.io" , address?.relay) assertEquals("wss://relay.damus.io", address?.relay)
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr()) assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr())
} }
@Test @Test
fun nAddrFormatter() { fun nAddrFormatter() {
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null) val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null)
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr()) assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
} }
@Test @Test
fun nAddrFormatter2() { fun nAddrFormatter2() {
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null) val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null)
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr()) assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
} }
@Test @Test
fun nAddrFormatter3() { fun nAddrFormatter3() {
val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io") val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io")
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr()) assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
} }
} }