2024-01-06 15:44:32 +00:00
|
|
|
/**
|
2024-02-15 23:31:26 +00:00
|
|
|
* Copyright (c) 2024 Vitor Pamplona
|
2024-01-06 15:44:32 +00:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
2023-01-11 18:31:20 +00:00
|
|
|
package com.vitorpamplona.amethyst.service.relays
|
|
|
|
|
2023-11-30 20:03:58 +00:00
|
|
|
import android.util.Log
|
2023-06-05 19:33:16 +00:00
|
|
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
2023-08-16 21:58:25 +00:00
|
|
|
import com.vitorpamplona.quartz.events.Event
|
|
|
|
import com.vitorpamplona.quartz.events.EventInterface
|
2023-03-13 17:47:44 +00:00
|
|
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
2023-02-19 00:08:52 +00:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2023-02-14 14:17:12 +00:00
|
|
|
import kotlinx.coroutines.GlobalScope
|
|
|
|
import kotlinx.coroutines.launch
|
2024-01-06 16:32:41 +00:00
|
|
|
import java.util.UUID
|
2023-01-11 18:31:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The Nostr Client manages multiple personae the user may switch between. Events are received and
|
2024-01-06 15:44:32 +00:00
|
|
|
* published through multiple relays. Events are stored with their respective persona.
|
2023-01-11 18:31:20 +00:00
|
|
|
*/
|
2023-03-07 18:46:44 +00:00
|
|
|
object Client : RelayPool.Listener {
|
2024-01-06 16:32:41 +00:00
|
|
|
private var listeners = setOf<Listener>()
|
|
|
|
private var relays = emptyArray<Relay>()
|
|
|
|
private var subscriptions = mapOf<String, List<TypedFilter>>()
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
fun reconnect(
|
|
|
|
relays: Array<Relay>?,
|
|
|
|
onlyIfChanged: Boolean = false,
|
|
|
|
) {
|
|
|
|
Log.d("Relay", "Relay Pool Reconnecting to ${relays?.size} relays")
|
|
|
|
checkNotInMainThread()
|
|
|
|
|
|
|
|
if (onlyIfChanged) {
|
|
|
|
if (!isSameRelaySetConfig(relays)) {
|
|
|
|
if (this.relays.isNotEmpty()) {
|
|
|
|
RelayPool.disconnect()
|
|
|
|
RelayPool.unregister(this)
|
|
|
|
RelayPool.unloadRelays()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relays != null) {
|
|
|
|
RelayPool.register(this)
|
|
|
|
RelayPool.loadRelays(relays.toList())
|
|
|
|
RelayPool.requestAndWatch()
|
|
|
|
this.relays = relays
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (this.relays.isNotEmpty()) {
|
|
|
|
RelayPool.disconnect()
|
|
|
|
RelayPool.unregister(this)
|
|
|
|
RelayPool.unloadRelays()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relays != null) {
|
|
|
|
RelayPool.register(this)
|
|
|
|
RelayPool.loadRelays(relays.toList())
|
|
|
|
RelayPool.requestAndWatch()
|
|
|
|
this.relays = relays
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun isSameRelaySetConfig(newRelayConfig: Array<Relay>?): Boolean {
|
|
|
|
if (relays.size != newRelayConfig?.size) return false
|
|
|
|
|
|
|
|
relays.forEach { oldRelayInfo ->
|
|
|
|
val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } ?: return false
|
|
|
|
|
|
|
|
if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
fun sendFilter(
|
|
|
|
subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
|
|
|
|
filters: List<TypedFilter> = listOf(),
|
|
|
|
) {
|
|
|
|
checkNotInMainThread()
|
|
|
|
|
|
|
|
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
2024-03-15 23:28:59 +00:00
|
|
|
RelayPool.sendFilter(subscriptionId, filters)
|
2024-01-06 16:32:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fun sendFilterOnlyIfDisconnected(
|
|
|
|
subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
|
|
|
|
filters: List<TypedFilter> = listOf(),
|
|
|
|
) {
|
|
|
|
checkNotInMainThread()
|
|
|
|
|
|
|
|
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
2024-02-21 15:04:40 +00:00
|
|
|
RelayPool.connectAndSendFiltersIfDisconnected()
|
2024-01-06 16:32:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fun send(
|
|
|
|
signedEvent: EventInterface,
|
|
|
|
relay: String? = null,
|
|
|
|
feedTypes: Set<FeedType>? = null,
|
|
|
|
relayList: List<Relay>? = null,
|
|
|
|
onDone: (() -> Unit)? = null,
|
|
|
|
) {
|
|
|
|
checkNotInMainThread()
|
|
|
|
|
|
|
|
if (relayList != null) {
|
|
|
|
RelayPool.sendToSelectedRelays(relayList, signedEvent)
|
|
|
|
} else if (relay == null) {
|
|
|
|
RelayPool.send(signedEvent)
|
|
|
|
} else {
|
2024-04-17 21:48:53 +00:00
|
|
|
RelayPool.getOrCreateRelay(relay, feedTypes, onDone) {
|
|
|
|
it.send(signedEvent)
|
2024-01-06 16:32:41 +00:00
|
|
|
}
|
2023-03-22 13:45:21 +00:00
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fun close(subscriptionId: String) {
|
|
|
|
RelayPool.close(subscriptionId)
|
|
|
|
subscriptions = subscriptions.minus(subscriptionId)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun isActive(subscriptionId: String): Boolean {
|
|
|
|
return subscriptions.contains(subscriptionId)
|
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2024-01-06 16:32:41 +00:00
|
|
|
@OptIn(DelicateCoroutinesApi::class)
|
|
|
|
override fun onEvent(
|
|
|
|
event: Event,
|
|
|
|
subscriptionId: String,
|
|
|
|
relay: Relay,
|
|
|
|
afterEOSE: Boolean,
|
|
|
|
) {
|
|
|
|
// Releases the Web thread for the new payload.
|
|
|
|
// May need to add a processing queue if processing new events become too costly.
|
|
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
|
|
listeners.forEach { it.onEvent(event, subscriptionId, relay, afterEOSE) }
|
2023-02-14 14:17:12 +00:00
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 16:32:41 +00:00
|
|
|
@OptIn(DelicateCoroutinesApi::class)
|
|
|
|
override fun onError(
|
|
|
|
error: Error,
|
|
|
|
subscriptionId: String,
|
|
|
|
relay: Relay,
|
|
|
|
) {
|
|
|
|
// Releases the Web thread for the new payload.
|
|
|
|
// May need to add a processing queue if processing new events become too costly.
|
|
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
|
|
listeners.forEach { it.onError(error, subscriptionId, relay) }
|
|
|
|
}
|
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2024-01-06 16:32:41 +00:00
|
|
|
@OptIn(DelicateCoroutinesApi::class)
|
|
|
|
override fun onRelayStateChange(
|
|
|
|
type: Relay.StateType,
|
|
|
|
relay: Relay,
|
|
|
|
channel: String?,
|
|
|
|
) {
|
|
|
|
// Releases the Web thread for the new payload.
|
|
|
|
// May need to add a processing queue if processing new events become too costly.
|
|
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
|
|
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
|
|
|
|
}
|
|
|
|
}
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2024-01-06 16:32:41 +00:00
|
|
|
@OptIn(DelicateCoroutinesApi::class)
|
|
|
|
override fun onSendResponse(
|
|
|
|
eventId: String,
|
|
|
|
success: Boolean,
|
|
|
|
message: String,
|
|
|
|
relay: Relay,
|
|
|
|
) {
|
|
|
|
// Releases the Web thread for the new payload.
|
|
|
|
// May need to add a processing queue if processing new events become too costly.
|
|
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
|
|
listeners.forEach { it.onSendResponse(eventId, success, message, relay) }
|
|
|
|
}
|
2023-01-14 02:35:28 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 16:32:41 +00:00
|
|
|
@OptIn(DelicateCoroutinesApi::class)
|
|
|
|
override fun onAuth(
|
|
|
|
relay: Relay,
|
|
|
|
challenge: String,
|
|
|
|
) {
|
|
|
|
// Releases the Web thread for the new payload.
|
|
|
|
// May need to add a processing queue if processing new events become too costly.
|
|
|
|
GlobalScope.launch(Dispatchers.Default) { listeners.forEach { it.onAuth(relay, challenge) } }
|
2023-04-26 01:18:33 +00:00
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
|
|
|
|
override fun onNotify(
|
|
|
|
relay: Relay,
|
|
|
|
description: String,
|
|
|
|
) {
|
|
|
|
// Releases the Web thread for the new payload.
|
|
|
|
// May need to add a processing queue if processing new events become too costly.
|
|
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
|
|
listeners.forEach { it.onNotify(relay, description) }
|
2023-11-26 23:00:08 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
|
|
|
|
fun subscribe(listener: Listener) {
|
|
|
|
listeners = listeners.plus(listener)
|
2023-12-26 23:01:31 +00:00
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
|
|
|
|
fun isSubscribed(listener: Listener): Boolean {
|
|
|
|
return listeners.contains(listener)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
|
|
|
|
fun unsubscribe(listener: Listener) {
|
|
|
|
listeners = listeners.minus(listener)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
|
2024-03-15 23:28:59 +00:00
|
|
|
fun allSubscriptions(): Map<String, List<TypedFilter>> {
|
|
|
|
return subscriptions
|
2024-01-06 16:32:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fun getSubscriptionFilters(subId: String): List<TypedFilter> {
|
|
|
|
return subscriptions[subId] ?: emptyList()
|
2023-01-17 13:42:00 +00:00
|
|
|
}
|
2024-01-06 16:32:41 +00:00
|
|
|
|
|
|
|
abstract class Listener {
|
|
|
|
/** A new message was received */
|
|
|
|
open fun onEvent(
|
|
|
|
event: Event,
|
|
|
|
subscriptionId: String,
|
|
|
|
relay: Relay,
|
|
|
|
afterEOSE: Boolean,
|
|
|
|
) = Unit
|
|
|
|
|
|
|
|
/** A new or repeat message was received */
|
|
|
|
open fun onError(
|
|
|
|
error: Error,
|
|
|
|
subscriptionId: String,
|
|
|
|
relay: Relay,
|
|
|
|
) = Unit
|
|
|
|
|
|
|
|
/** Connected to or disconnected from a relay */
|
|
|
|
open fun onRelayStateChange(
|
|
|
|
type: Relay.StateType,
|
|
|
|
relay: Relay,
|
2024-02-21 15:04:40 +00:00
|
|
|
subscriptionId: String?,
|
2024-01-06 16:32:41 +00:00
|
|
|
) = Unit
|
|
|
|
|
|
|
|
/** When an relay saves or rejects a new event. */
|
|
|
|
open fun onSendResponse(
|
|
|
|
eventId: String,
|
|
|
|
success: Boolean,
|
|
|
|
message: String,
|
|
|
|
relay: Relay,
|
|
|
|
) = Unit
|
|
|
|
|
|
|
|
open fun onAuth(
|
|
|
|
relay: Relay,
|
|
|
|
challenge: String,
|
|
|
|
) = Unit
|
|
|
|
|
|
|
|
open fun onNotify(
|
|
|
|
relay: Relay,
|
|
|
|
description: String,
|
|
|
|
) = Unit
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
2023-03-05 21:42:19 +00:00
|
|
|
}
|