Merge branch 'vitorpamplona:main' into profiles-list-management
This commit is contained in:
+13
-6
@@ -122,8 +122,6 @@ class NostrClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun allAvailableRelays() = relayPool.getAll()
|
||||
|
||||
// Reconnects all relays that may have disconnected
|
||||
fun connect() {
|
||||
isActive = true
|
||||
@@ -138,10 +136,7 @@ class NostrClient(
|
||||
@Synchronized
|
||||
fun reconnect(onlyIfChanged: Boolean = false) {
|
||||
if (onlyIfChanged) {
|
||||
relayPool.getAllNeedsToReconnect().forEach {
|
||||
it.disconnect()
|
||||
}
|
||||
relayPool.connect()
|
||||
relayPool.reconnectIfNeedsToORIfItIsTime()
|
||||
} else {
|
||||
relayPool.disconnect()
|
||||
relayPool.connect()
|
||||
@@ -236,6 +231,9 @@ class NostrClient(
|
||||
relayPool.connectIfDisconnected(relay)
|
||||
}
|
||||
}
|
||||
|
||||
// wakes up all the other relays
|
||||
relayPool.reconnectIfNeedsToORIfItIsTime()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +267,9 @@ class NostrClient(
|
||||
relayPool.connectIfDisconnected(relay)
|
||||
}
|
||||
}
|
||||
|
||||
// wakes up all the other relays
|
||||
relayPool.reconnectIfNeedsToORIfItIsTime()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +279,9 @@ class NostrClient(
|
||||
) {
|
||||
if (isActive) {
|
||||
relayPool.getRelay(connectedRelay)?.send(event)
|
||||
|
||||
// wakes up all the other relays
|
||||
relayPool.reconnectIfNeedsToORIfItIsTime()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +292,9 @@ class NostrClient(
|
||||
eventOutbox.markAsSending(event, relayList)
|
||||
if (isActive) {
|
||||
relayPool.send(event, relayList)
|
||||
|
||||
// wakes up all the other relays
|
||||
relayPool.reconnectIfNeedsToORIfItIsTime()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
class NostrClientSubscription(
|
||||
val client: NostrClient,
|
||||
val filter: () -> Map<NormalizedRelayUrl, List<Filter>> = { emptyMap() },
|
||||
val onEvent: (event: Event) -> Unit = {},
|
||||
) : IRelayClientListener {
|
||||
private val subId = RandomInstance.randomChars(10)
|
||||
|
||||
init {
|
||||
client.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onEvent(
|
||||
relay: IRelayClient,
|
||||
subId: String,
|
||||
event: Event,
|
||||
arrivalTime: Long,
|
||||
afterEOSE: Boolean,
|
||||
) {
|
||||
if (this.subId == subId) {
|
||||
onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or Updates the filter with relays. This method should be called
|
||||
* everytime the filter changes.
|
||||
*/
|
||||
fun updateFilter() = client.openReqSubscription(subId, filter())
|
||||
|
||||
fun closeSubscription() = client.close(subId)
|
||||
}
|
||||
+17
-8
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -70,16 +71,24 @@ class RelayPool(
|
||||
|
||||
fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url)
|
||||
|
||||
fun getAll() = statusFlow.value.connected
|
||||
var lastReconnectCall = TimeUtils.now()
|
||||
|
||||
fun getAllNeedsToReconnect() = relays.filter { url, relay -> relay.needsToReconnect() }
|
||||
|
||||
fun reconnectsRelaysThatNeedTo() {
|
||||
relays.forEach { url, relay ->
|
||||
if (relay.needsToReconnect()) {
|
||||
relay.disconnect()
|
||||
relay.connect()
|
||||
fun reconnectIfNeedsToORIfItIsTime() {
|
||||
if (lastReconnectCall < TimeUtils.tenSecondsAgo()) {
|
||||
relays.forEach { url, relay ->
|
||||
if (relay.isConnected()) {
|
||||
if (relay.needsToReconnect()) {
|
||||
// network has changed, force reconnect
|
||||
relay.disconnect()
|
||||
relay.connect()
|
||||
}
|
||||
} else {
|
||||
// relay is not connected. Connect if it is time
|
||||
relay.connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
}
|
||||
|
||||
lastReconnectCall = TimeUtils.now()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+47
-41
@@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient.Companion.DELAY_TO_RECONNECT_IN_MSECS
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage
|
||||
@@ -84,7 +83,7 @@ open class BasicRelayClient(
|
||||
) : IRelayClient {
|
||||
companion object {
|
||||
// waits 3 minutes to reconnect once things fail
|
||||
const val DELAY_TO_RECONNECT_IN_MSECS = 500L
|
||||
const val DELAY_TO_RECONNECT_IN_SECS = 1
|
||||
const val EVENT_MESSAGE_PREFIX = "[\"${EventMessage.LABEL}\""
|
||||
}
|
||||
|
||||
@@ -94,8 +93,8 @@ open class BasicRelayClient(
|
||||
private var isReady: Boolean = false
|
||||
private var usingCompression: Boolean = false
|
||||
|
||||
private var lastConnectTentative: Long = 0L // the beginning of time.
|
||||
private var delayToConnect = DELAY_TO_RECONNECT_IN_MSECS
|
||||
private var lastConnectTentativeInSeconds: Long = 0L // the beginning of time.
|
||||
private var delayToConnectInSeconds = DELAY_TO_RECONNECT_IN_SECS
|
||||
|
||||
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
||||
|
||||
@@ -138,7 +137,7 @@ open class BasicRelayClient(
|
||||
|
||||
Log.d(logTag, "Connecting...")
|
||||
|
||||
lastConnectTentative = TimeUtils.now()
|
||||
lastConnectTentativeInSeconds = TimeUtils.now()
|
||||
|
||||
socket = socketBuilder.build(url, MyWebsocketListener(onConnected))
|
||||
socket?.connect()
|
||||
@@ -207,22 +206,30 @@ open class BasicRelayClient(
|
||||
code: Int?,
|
||||
response: String?,
|
||||
) {
|
||||
socket?.disconnect() // 1000, "Normal close"
|
||||
// socket is already closed
|
||||
// socket?.disconnect()
|
||||
|
||||
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
|
||||
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
|
||||
stats.newError(response ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
|
||||
if (socket == null) {
|
||||
// comes from the disconnect action.
|
||||
listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTED)
|
||||
} else {
|
||||
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
|
||||
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
|
||||
stats.newError(response ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
// Failures disconnect the relay.
|
||||
markConnectionAsClosed()
|
||||
|
||||
Log.w(logTag, "OnFailure $code $response ${t.message}")
|
||||
|
||||
if (code == 403 || code == 502 || code == 503 || code == 402 || code == 410 || t.message == "SOCKS: Host unreachable") {
|
||||
dontTryAgainForALongTime()
|
||||
}
|
||||
|
||||
listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTED)
|
||||
listener.onError(this@BasicRelayClient, "", Error("WebSocket Failure. Response: $code $response. Exception: ${t.message}", t))
|
||||
}
|
||||
|
||||
// Failures disconnect the relay.
|
||||
markConnectionAsClosed()
|
||||
|
||||
Log.w(logTag, "OnFailure $code $response ${t.message} $socket")
|
||||
listener.onError(
|
||||
this@BasicRelayClient,
|
||||
"",
|
||||
Error("WebSocket Failure. Response: $code $response. Exception: ${t.message}", t),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +268,7 @@ open class BasicRelayClient(
|
||||
this.usingCompression = usingCompression
|
||||
|
||||
// resets any extra delays added during on offline state
|
||||
this.delayToConnect = DELAY_TO_RECONNECT_IN_MSECS
|
||||
this.delayToConnectInSeconds = DELAY_TO_RECONNECT_IN_SECS
|
||||
|
||||
stats.pingInMs = pingInMs
|
||||
}
|
||||
@@ -320,7 +327,7 @@ open class BasicRelayClient(
|
||||
}
|
||||
|
||||
private fun processAuth(msg: AuthMessage) {
|
||||
// Log.d(logTag, "Auth $newMessage")
|
||||
Log.d(logTag, "Auth ${msg.challenge}")
|
||||
listener.onAuth(this@BasicRelayClient, msg.challenge)
|
||||
}
|
||||
|
||||
@@ -344,8 +351,8 @@ open class BasicRelayClient(
|
||||
|
||||
override fun disconnect() {
|
||||
Log.d(logTag, "Disconnecting...")
|
||||
lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested.
|
||||
delayToConnect = DELAY_TO_RECONNECT_IN_MSECS
|
||||
lastConnectTentativeInSeconds = 0L // this is not an error, so prepare to reconnect as soon as requested.
|
||||
delayToConnectInSeconds = DELAY_TO_RECONNECT_IN_SECS
|
||||
socket?.disconnect()
|
||||
socket = null
|
||||
isReady = false
|
||||
@@ -372,12 +379,7 @@ open class BasicRelayClient(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// waits 60 seconds to reconnect after disconnected.
|
||||
if (TimeUtils.now() > lastConnectTentative + delayToConnect) {
|
||||
delayToConnect = delayToConnect * 2
|
||||
// sends all filters after connection is successful.
|
||||
connect()
|
||||
}
|
||||
connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,25 +395,30 @@ open class BasicRelayClient(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
}
|
||||
|
||||
override fun connectAndSyncFiltersIfDisconnected() {
|
||||
if (!isConnectionStarted() && !connectingMutex.get()) {
|
||||
// waits 60 seconds to reconnect after disconnected.
|
||||
if (TimeUtils.now() > lastConnectTentative + delayToConnect) {
|
||||
// sends all filters after connection is successful.
|
||||
delayToConnect = delayToConnect * 2
|
||||
if (TimeUtils.now() > lastConnectTentativeInSeconds + delayToConnectInSeconds) {
|
||||
upRelayDelayToConnect()
|
||||
connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun connectAndSyncFiltersIfDisconnected() {
|
||||
if (socket == null) {
|
||||
// waits 60 seconds to reconnect after disconnected.
|
||||
if (TimeUtils.now() > lastConnectTentative + delayToConnect) {
|
||||
delayToConnect = delayToConnect * 2
|
||||
connect()
|
||||
}
|
||||
fun upRelayDelayToConnect() {
|
||||
if (delayToConnectInSeconds < TimeUtils.FIVE_MINUTES) {
|
||||
delayToConnectInSeconds = delayToConnectInSeconds * 2
|
||||
}
|
||||
}
|
||||
|
||||
fun dontTryAgainForALongTime() {
|
||||
delayToConnectInSeconds = TimeUtils.ONE_DAY
|
||||
}
|
||||
|
||||
override fun send(event: Event) {
|
||||
listener.onBeforeSend(this@BasicRelayClient, event)
|
||||
|
||||
@@ -439,8 +446,7 @@ open class BasicRelayClient(
|
||||
writeToSocket(EventCmd.Companion.toJson(event))
|
||||
}
|
||||
} else {
|
||||
// automatically sends all filters after connection is successful.
|
||||
connect()
|
||||
connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -29,9 +29,6 @@ class NoticeMessage(
|
||||
const val LABEL = "NOTICE"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(msgArray: JsonNode): NoticeMessage =
|
||||
NoticeMessage(
|
||||
msgArray.get(1).asText(),
|
||||
)
|
||||
fun parse(msgArray: JsonNode) = NoticeMessage(msgArray.get(1).asText())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ fun NormalizedRelayUrl.toHttp() =
|
||||
if (url.startsWith("wss://")) {
|
||||
"https${url.drop(3)}"
|
||||
} else if (url.startsWith("ws://")) {
|
||||
"https${url.drop(2)}"
|
||||
"http${url.drop(2)}"
|
||||
} else {
|
||||
"https://$url"
|
||||
}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class BasicOkHttpWebSocket(
|
||||
val url: NormalizedRelayUrl,
|
||||
val httpClient: OkHttpClient,
|
||||
val out: WebSocketListener,
|
||||
) : WebSocket {
|
||||
private var socket: okhttp3.WebSocket? = null
|
||||
|
||||
override fun needsReconnect() = socket == null
|
||||
|
||||
override fun connect() {
|
||||
val request = Request.Builder().url(url.url).build()
|
||||
|
||||
val listener =
|
||||
object : okhttp3.WebSocketListener() {
|
||||
override fun onOpen(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
response: Response,
|
||||
) = out.onOpen(
|
||||
response.receivedResponseAtMillis - response.sentRequestAtMillis,
|
||||
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
|
||||
)
|
||||
|
||||
override fun onMessage(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
text: String,
|
||||
) = out.onMessage(text)
|
||||
|
||||
override fun onClosing(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
code: Int,
|
||||
reason: String,
|
||||
) = out.onClosing(code, reason)
|
||||
|
||||
override fun onClosed(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
code: Int,
|
||||
reason: String,
|
||||
) = out.onClosed(code, reason)
|
||||
|
||||
override fun onFailure(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
t: Throwable,
|
||||
r: Response?,
|
||||
) = out.onFailure(t, r?.code, r?.message)
|
||||
}
|
||||
|
||||
socket = httpClient.newWebSocket(request, listener)
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
socket?.cancel()
|
||||
socket = null
|
||||
}
|
||||
|
||||
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
|
||||
|
||||
class Builder(
|
||||
val httpClient: OkHttpClient,
|
||||
) : WebsocketBuilder {
|
||||
// Called when connecting.
|
||||
override fun build(
|
||||
url: NormalizedRelayUrl,
|
||||
out: WebSocketListener,
|
||||
) = BasicOkHttpWebSocket(url, httpClient, out)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.tags.publishedAt
|
||||
|
||||
interface PublishedAtProvider {
|
||||
fun publishedAt(): Long?
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.addressHints
|
||||
@@ -68,6 +69,7 @@ class LongTextNoteEvent(
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider,
|
||||
AddressHintProvider,
|
||||
PublishedAtProvider,
|
||||
RootScope,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content
|
||||
@@ -130,7 +132,7 @@ class LongTextNoteEvent(
|
||||
|
||||
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
|
||||
|
||||
fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30023
|
||||
|
||||
+10
-1
@@ -32,7 +32,16 @@ class PublishedAtTag {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toLongOrNull()
|
||||
val timestamp = tag[1].toLongOrNull()
|
||||
|
||||
if (timestamp == null) return null
|
||||
|
||||
if (timestamp > 3_000_000_000) {
|
||||
// like in milliseconds
|
||||
return timestamp / 1000
|
||||
} else {
|
||||
return timestamp
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
||||
@@ -62,6 +63,7 @@ class WikiNoteEvent(
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider,
|
||||
PublishedAtProvider,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content
|
||||
|
||||
@@ -127,7 +129,7 @@ class WikiNoteEvent(
|
||||
|
||||
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
||||
|
||||
fun publishedAt() =
|
||||
override fun publishedAt() =
|
||||
try {
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
|
||||
} catch (_: Exception) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip22Comments.RootScope
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||
@@ -45,12 +46,13 @@ abstract class VideoEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig),
|
||||
PublishedAtProvider,
|
||||
RootScope {
|
||||
@Transient var iMetas: List<VideoMeta>? = null
|
||||
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
|
||||
fun duration() = tags.firstNotNullOfOrNull(DurationTag::parse)
|
||||
|
||||
|
||||
+4
-2
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.isTaggedKind
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
@@ -46,7 +47,8 @@ class AppDefinitionEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PublishedAtProvider {
|
||||
override fun countMemory(): Long = super.countMemory() + (cachedMetadata?.countMemory() ?: 8L)
|
||||
|
||||
@Transient private var cachedMetadata: AppMetadata? = null
|
||||
@@ -77,7 +79,7 @@ class AppDefinitionEvent(
|
||||
|
||||
fun includeKind(kind: Int) = tags.isTaggedKind(kind)
|
||||
|
||||
fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 31990
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@ class AppMetadata {
|
||||
fun toJson() = assemble(this)
|
||||
|
||||
companion object {
|
||||
fun assemble(data: AppMetadata) = JsonMapper.mapper.writeValueAsString(data)
|
||||
fun assemble(data: AppMetadata): String = JsonMapper.mapper.writeValueAsString(data)
|
||||
|
||||
fun parse(content: String) = JsonMapper.mapper.readValue(content, AppMetadata::class.java)
|
||||
fun parse(content: String): AppMetadata = JsonMapper.mapper.readValue(content, AppMetadata::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.containsAllTagNamesWithValues
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
|
||||
@@ -49,7 +50,8 @@ class ClassifiedsEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PublishedAtProvider {
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||
@@ -68,7 +70,7 @@ class ClassifiedsEvent(
|
||||
|
||||
fun location() = tags.firstNotNullOfOrNull(LocationTag::parse)
|
||||
|
||||
fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
override fun publishedAt() = tags.firstNotNullOfOrNull(PublishedAtTag::parse)
|
||||
|
||||
fun categories() = tags.hashtags()
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ object TimeUtils {
|
||||
|
||||
fun tenSecondsFromNow() = now() + TEN_SECONDS
|
||||
|
||||
fun tenSecondsAgo() = now() - TEN_SECONDS
|
||||
|
||||
fun oneMinuteFromNow() = now() + ONE_MINUTE
|
||||
|
||||
fun oneMinuteAgo() = now() - ONE_MINUTE
|
||||
|
||||
Reference in New Issue
Block a user