mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-15 08:47:33 +01:00
Refactors Ammolite to remove the dependency on OkHttp to prepare for KTor and multiplatform settings.
- This also reduces the Singleton coupling between Client and RelayPool. To migrate, create a NostrClient instance on your Application class and update your code to access that `client` instance.
This commit is contained in:
@@ -57,7 +57,6 @@ dependencies {
|
||||
implementation libs.androidx.runtime.runtime
|
||||
|
||||
implementation project(path: ':quartz')
|
||||
implementation libs.okhttp
|
||||
|
||||
testImplementation libs.junit
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.ammolite.relays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -37,10 +38,16 @@ import java.util.concurrent.TimeUnit
|
||||
* The Nostr Client manages multiple personae the user may switch between. Events are received and
|
||||
* published through multiple relays. Events are stored with their respective persona.
|
||||
*/
|
||||
object Client : RelayPool.Listener {
|
||||
class NostrClient(
|
||||
private val websocketBuilder: WebsocketBuilder,
|
||||
) : RelayPool.Listener {
|
||||
private val relayPool: RelayPool = RelayPool()
|
||||
private val subscriptions: MutableSubscriptionManager = MutableSubscriptionManager()
|
||||
|
||||
private var listeners = setOf<Listener>()
|
||||
private var relays = emptyArray<Relay>()
|
||||
private var subscriptions = mapOf<String, List<TypedFilter>>()
|
||||
|
||||
fun buildRelay(it: RelaySetupInfoToConnect): Relay = Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes, websocketBuilder, subscriptions)
|
||||
|
||||
@Synchronized
|
||||
fun reconnect(
|
||||
@@ -52,33 +59,33 @@ object Client : RelayPool.Listener {
|
||||
|
||||
if (onlyIfChanged) {
|
||||
if (!isSameRelaySetConfig(relays)) {
|
||||
if (Client.relays.isNotEmpty()) {
|
||||
RelayPool.disconnect()
|
||||
RelayPool.unregister(this)
|
||||
RelayPool.unloadRelays()
|
||||
if (this.relays.isNotEmpty()) {
|
||||
relayPool.disconnect()
|
||||
relayPool.unregister(this)
|
||||
relayPool.unloadRelays()
|
||||
}
|
||||
|
||||
if (relays != null) {
|
||||
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes) }
|
||||
RelayPool.register(this)
|
||||
RelayPool.loadRelays(newRelays)
|
||||
RelayPool.requestAndWatch()
|
||||
Client.relays = newRelays.toTypedArray()
|
||||
val newRelays = relays.map(::buildRelay)
|
||||
relayPool.register(this)
|
||||
relayPool.loadRelays(newRelays)
|
||||
relayPool.requestAndWatch()
|
||||
this.relays = newRelays.toTypedArray()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Client.relays.isNotEmpty()) {
|
||||
RelayPool.disconnect()
|
||||
RelayPool.unregister(this)
|
||||
RelayPool.unloadRelays()
|
||||
if (this.relays.isNotEmpty()) {
|
||||
relayPool.disconnect()
|
||||
relayPool.unregister(this)
|
||||
relayPool.unloadRelays()
|
||||
}
|
||||
|
||||
if (relays != null) {
|
||||
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes) }
|
||||
RelayPool.register(this)
|
||||
RelayPool.loadRelays(newRelays)
|
||||
RelayPool.requestAndWatch()
|
||||
Client.relays = newRelays.toTypedArray()
|
||||
val newRelays = relays.map(::buildRelay)
|
||||
relayPool.register(this)
|
||||
relayPool.loadRelays(newRelays)
|
||||
relayPool.requestAndWatch()
|
||||
this.relays = newRelays.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,8 +108,8 @@ object Client : RelayPool.Listener {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||
RelayPool.sendFilter(subscriptionId, filters)
|
||||
subscriptions.add(subscriptionId, filters)
|
||||
relayPool.sendFilter(subscriptionId, filters)
|
||||
}
|
||||
|
||||
fun sendFilterAndStopOnFirstResponse(
|
||||
@@ -129,8 +136,8 @@ object Client : RelayPool.Listener {
|
||||
},
|
||||
)
|
||||
|
||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||
RelayPool.sendFilter(subscriptionId, filters)
|
||||
subscriptions.add(subscriptionId, filters)
|
||||
relayPool.sendFilter(subscriptionId, filters)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@@ -146,7 +153,7 @@ object Client : RelayPool.Listener {
|
||||
): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
val size = if (relay != null) 1 else relayList?.size ?: RelayPool.availableRelays()
|
||||
val size = if (relay != null) 1 else relayList?.size ?: relayPool.availableRelays()
|
||||
val latch = CountDownLatch(size)
|
||||
val relayErrors = mutableMapOf<String, String>()
|
||||
var result = false
|
||||
@@ -227,8 +234,8 @@ object Client : RelayPool.Listener {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||
RelayPool.connectAndSendFiltersIfDisconnected()
|
||||
subscriptions.add(subscriptionId, filters)
|
||||
relayPool.connectAndSendFiltersIfDisconnected()
|
||||
}
|
||||
|
||||
fun sendIfExists(
|
||||
@@ -237,7 +244,7 @@ object Client : RelayPool.Listener {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayPool.getRelays(connectedRelay.url).forEach {
|
||||
relayPool.getRelays(connectedRelay.url).forEach {
|
||||
it.send(signedEvent)
|
||||
}
|
||||
}
|
||||
@@ -249,14 +256,14 @@ object Client : RelayPool.Listener {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayPool.getOrCreateRelay(relayTemplate, onDone) {
|
||||
relayPool.runCreatingIfNeeded(buildRelay(relayTemplate), onDone = onDone) {
|
||||
it.send(signedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
fun send(signedEvent: EventInterface) {
|
||||
checkNotInMainThread()
|
||||
RelayPool.send(signedEvent)
|
||||
relayPool.send(signedEvent)
|
||||
}
|
||||
|
||||
fun send(
|
||||
@@ -265,7 +272,7 @@ object Client : RelayPool.Listener {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayPool.sendToSelectedRelays(relayList, signedEvent)
|
||||
relayPool.sendToSelectedRelays(relayList, signedEvent)
|
||||
}
|
||||
|
||||
fun sendPrivately(
|
||||
@@ -275,18 +282,18 @@ object Client : RelayPool.Listener {
|
||||
checkNotInMainThread()
|
||||
|
||||
relayList.forEach { relayTemplate ->
|
||||
RelayPool.getOrCreateRelay(relayTemplate, { }) {
|
||||
relayPool.runCreatingIfNeeded(buildRelay(relayTemplate)) {
|
||||
it.sendOverride(signedEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun close(subscriptionId: String) {
|
||||
RelayPool.close(subscriptionId)
|
||||
subscriptions = subscriptions.minus(subscriptionId)
|
||||
relayPool.close(subscriptionId)
|
||||
subscriptions.remove(subscriptionId)
|
||||
}
|
||||
|
||||
fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId)
|
||||
fun isActive(subscriptionId: String): Boolean = subscriptions.isActive(subscriptionId)
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onEvent(
|
||||
@@ -392,9 +399,13 @@ object Client : RelayPool.Listener {
|
||||
listeners = listeners.minus(listener)
|
||||
}
|
||||
|
||||
fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions
|
||||
fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions.allSubscriptions()
|
||||
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions.getSubscriptionFilters(subId)
|
||||
|
||||
fun connectedRelays() = relayPool.connectedRelays()
|
||||
|
||||
fun relayStatusFlow() = relayPool.statusFlow
|
||||
|
||||
interface Listener {
|
||||
/** A new message was received */
|
||||
@@ -35,6 +35,7 @@ import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
abstract class NostrDataSource(
|
||||
val client: NostrClient,
|
||||
val debugName: String,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
@@ -67,7 +68,7 @@ abstract class NostrDataSource(
|
||||
): Int = 31 * str1.hashCode() + str2.hashCode()
|
||||
|
||||
private val clientListener =
|
||||
object : Client.Listener {
|
||||
object : NostrClient.Listener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
subscriptionId: String,
|
||||
@@ -139,14 +140,14 @@ abstract class NostrDataSource(
|
||||
|
||||
init {
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Subscribe")
|
||||
Client.subscribe(clientListener)
|
||||
client.subscribe(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Unsubscribe")
|
||||
stop()
|
||||
Client.unsubscribe(clientListener)
|
||||
client.unsubscribe(clientListener)
|
||||
scope.cancel()
|
||||
bundler.cancel()
|
||||
}
|
||||
@@ -170,7 +171,7 @@ abstract class NostrDataSource(
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
subscriptions.values.forEach { subscription ->
|
||||
Client.close(subscription.id)
|
||||
client.close(subscription.id)
|
||||
subscription.typedFilters = null
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ abstract class NostrDataSource(
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Stop")
|
||||
|
||||
subscriptions.values.forEach { subscription ->
|
||||
Client.close(subscription.id)
|
||||
client.close(subscription.id)
|
||||
subscription.typedFilters = null
|
||||
}
|
||||
}
|
||||
@@ -193,7 +194,7 @@ abstract class NostrDataSource(
|
||||
}
|
||||
|
||||
fun dismissChannel(subscription: Subscription) {
|
||||
Client.close(subscription.id)
|
||||
client.close(subscription.id)
|
||||
subscriptions = subscriptions.minus(subscription.id)
|
||||
}
|
||||
|
||||
@@ -231,29 +232,29 @@ abstract class NostrDataSource(
|
||||
subscriptions.values.forEach { updatedSubscription ->
|
||||
val updatedSubscriptionNewFilters = updatedSubscription.typedFilters
|
||||
|
||||
val isActive = Client.isActive(updatedSubscription.id)
|
||||
val isActive = client.isActive(updatedSubscription.id)
|
||||
|
||||
if (!isActive && updatedSubscriptionNewFilters != null) {
|
||||
// Filter was removed from the active list
|
||||
if (active) {
|
||||
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
}
|
||||
} else {
|
||||
if (currentFilters.containsKey(updatedSubscription.id)) {
|
||||
if (updatedSubscriptionNewFilters == null) {
|
||||
// was active and is not active anymore, just close.
|
||||
Client.close(updatedSubscription.id)
|
||||
client.close(updatedSubscription.id)
|
||||
} else {
|
||||
// was active and is still active, check if it has changed.
|
||||
if (updatedSubscription.hasChangedFiltersFrom(currentFilters[updatedSubscription.id])) {
|
||||
Client.close(updatedSubscription.id)
|
||||
client.close(updatedSubscription.id)
|
||||
if (active) {
|
||||
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
}
|
||||
} else {
|
||||
// hasn't changed, does nothing.
|
||||
if (active) {
|
||||
Client.sendFilterOnlyIfDisconnected(
|
||||
client.sendFilterOnlyIfDisconnected(
|
||||
updatedSubscription.id,
|
||||
updatedSubscriptionNewFilters,
|
||||
)
|
||||
@@ -269,9 +270,9 @@ abstract class NostrDataSource(
|
||||
if (active) {
|
||||
Log.d(
|
||||
this@NostrDataSource.javaClass.simpleName,
|
||||
"Update Filter 3 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}",
|
||||
"Update Filter 3 ${updatedSubscription.id} ${client.isSubscribed(clientListener)}",
|
||||
)
|
||||
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ package com.vitorpamplona.ammolite.relays
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.ammolite.BuildConfig
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||
import com.vitorpamplona.ammolite.sockets.WebSocket
|
||||
import com.vitorpamplona.ammolite.sockets.WebSocketListener
|
||||
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
@@ -31,10 +33,6 @@ import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
enum class FeedType {
|
||||
@@ -61,6 +59,8 @@ class Relay(
|
||||
val write: Boolean = true,
|
||||
val forceProxy: Boolean = false,
|
||||
val activeTypes: Set<FeedType>,
|
||||
val socketBuilder: WebsocketBuilder,
|
||||
val subs: SubscriptionManager,
|
||||
) {
|
||||
companion object {
|
||||
// waits 3 minutes to reconnect once things fail
|
||||
@@ -132,13 +132,7 @@ class Relay(
|
||||
|
||||
lastConnectTentative = TimeUtils.now()
|
||||
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url.trim())
|
||||
.build()
|
||||
|
||||
socket = HttpClientManager.getHttpClient(forceProxy).newWebSocket(request, RelayListener(onConnected))
|
||||
socket = socketBuilder.build(url, false, RelayListener(onConnected))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -153,19 +147,15 @@ class Relay(
|
||||
|
||||
inner class RelayListener(
|
||||
val onConnected: (Relay) -> Unit,
|
||||
) : WebSocketListener() {
|
||||
) : WebSocketListener {
|
||||
override fun onOpen(
|
||||
webSocket: WebSocket,
|
||||
response: Response,
|
||||
pingInMs: Long,
|
||||
usingCompression: Boolean,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
Log.d("Relay", "Connect onOpen $url $socket")
|
||||
|
||||
markConnectionAsReady(
|
||||
pingInMs = response.receivedResponseAtMillis - response.sentRequestAtMillis,
|
||||
usingCompression =
|
||||
response.headers.get("Sec-WebSocket-Extensions")?.contains("permessage-deflate") ?: false,
|
||||
)
|
||||
markConnectionAsReady(pingInMs, usingCompression)
|
||||
|
||||
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
|
||||
onConnected(this@Relay)
|
||||
@@ -173,10 +163,7 @@ class Relay(
|
||||
listeners.forEach { it.onRelayStateChange(this@Relay, StateType.CONNECT, null) }
|
||||
}
|
||||
|
||||
override fun onMessage(
|
||||
webSocket: WebSocket,
|
||||
text: String,
|
||||
) {
|
||||
override fun onMessage(text: String) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayStats.addBytesReceived(url, text.bytesUsedInMemory())
|
||||
@@ -193,7 +180,6 @@ class Relay(
|
||||
}
|
||||
|
||||
override fun onClosing(
|
||||
webSocket: WebSocket,
|
||||
code: Int,
|
||||
reason: String,
|
||||
) {
|
||||
@@ -211,7 +197,6 @@ class Relay(
|
||||
}
|
||||
|
||||
override fun onClosed(
|
||||
webSocket: WebSocket,
|
||||
code: Int,
|
||||
reason: String,
|
||||
) {
|
||||
@@ -225,9 +210,8 @@ class Relay(
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
webSocket: WebSocket,
|
||||
t: Throwable,
|
||||
response: Response?,
|
||||
responseMessage: String?,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -235,19 +219,19 @@ class Relay(
|
||||
|
||||
// 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"))) {
|
||||
RelayStats.newError(url, response?.message ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
|
||||
RelayStats.newError(url, responseMessage ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
// Failures disconnect the relay.
|
||||
markConnectionAsClosed()
|
||||
|
||||
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket")
|
||||
Log.w("Relay", "Relay onFailure $url, $responseMessage $responseMessage ${t.message} $socket")
|
||||
t.printStackTrace()
|
||||
listeners.forEach {
|
||||
it.onError(
|
||||
this@Relay,
|
||||
"",
|
||||
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
|
||||
Error("WebSocket Failure. Response: $responseMessage. Exception: ${t.message}", t),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -461,7 +445,7 @@ class Relay(
|
||||
|
||||
fun renewFilters() {
|
||||
// Force update all filters after AUTH.
|
||||
Client.allSubscriptions().forEach {
|
||||
subs.allSubscriptions().forEach {
|
||||
sendFilter(requestId = it.key, it.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@@ -37,7 +36,7 @@ import kotlinx.coroutines.launch
|
||||
/**
|
||||
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
|
||||
*/
|
||||
object RelayPool : Relay.Listener {
|
||||
class RelayPool : Relay.Listener {
|
||||
private var relays = listOf<Relay>()
|
||||
private var listeners = setOf<Listener>()
|
||||
|
||||
@@ -57,57 +56,34 @@ object RelayPool : Relay.Listener {
|
||||
|
||||
fun getAll() = relays
|
||||
|
||||
fun getOrCreateRelay(
|
||||
relayTemplate: RelaySetupInfoToConnect,
|
||||
fun runCreatingIfNeeded(
|
||||
relay: Relay,
|
||||
timeout: Long = 60000,
|
||||
onDone: (() -> Unit)? = null,
|
||||
whenConnected: (Relay) -> Unit,
|
||||
) {
|
||||
synchronized(this) {
|
||||
val matching = getRelays(relayTemplate.url)
|
||||
val matching = getRelays(relay.url)
|
||||
if (matching.isNotEmpty()) {
|
||||
matching.forEach { whenConnected(it) }
|
||||
} else {
|
||||
/** temporary connection */
|
||||
newSporadicRelay(
|
||||
relayTemplate.url,
|
||||
relayTemplate.read,
|
||||
relayTemplate.write,
|
||||
relayTemplate.forceProxy,
|
||||
relayTemplate.feedTypes,
|
||||
onConnected = whenConnected,
|
||||
onDone = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
addRelay(relay)
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun newSporadicRelay(
|
||||
url: String,
|
||||
read: Boolean,
|
||||
write: Boolean,
|
||||
forceProxy: Boolean,
|
||||
feedTypes: Set<FeedType>?,
|
||||
onConnected: (Relay) -> Unit,
|
||||
onDone: (() -> Unit)?,
|
||||
timeout: Long = 60000,
|
||||
) {
|
||||
val relay = Relay(url, read, write, forceProxy, feedTypes ?: emptySet())
|
||||
addRelay(relay)
|
||||
relay.connectAndRun {
|
||||
relay.renewFilters()
|
||||
relay.sendOutbox()
|
||||
|
||||
relay.connectAndRun {
|
||||
relay.renewFilters()
|
||||
relay.sendOutbox()
|
||||
whenConnected(relay)
|
||||
|
||||
onConnected(relay)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
delay(timeout) // waits for a reply
|
||||
relay.disconnect()
|
||||
removeRelay(relay)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
delay(timeout) // waits for a reply
|
||||
relay.disconnect()
|
||||
removeRelay(relay)
|
||||
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.relays
|
||||
|
||||
class MutableSubscriptionManager : SubscriptionManager {
|
||||
private var subscriptions = mapOf<String, List<TypedFilter>>()
|
||||
|
||||
fun add(
|
||||
subscriptionId: String,
|
||||
filters: List<TypedFilter> = listOf(),
|
||||
) {
|
||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||
}
|
||||
|
||||
fun remove(subscriptionId: String) {
|
||||
subscriptions = subscriptions.minus(subscriptionId)
|
||||
}
|
||||
|
||||
override fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId)
|
||||
|
||||
override fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions
|
||||
|
||||
override fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
|
||||
}
|
||||
|
||||
interface SubscriptionManager {
|
||||
fun isActive(subscriptionId: String): Boolean
|
||||
|
||||
fun allSubscriptions(): Map<String, List<TypedFilter>>
|
||||
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter>
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.service
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager.setDefaultProxy
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.time.Duration
|
||||
|
||||
class LoggingInterceptor : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request: Request = chain.request()
|
||||
val t1 = System.nanoTime()
|
||||
val port =
|
||||
(
|
||||
chain
|
||||
.connection()
|
||||
?.route()
|
||||
?.proxy
|
||||
?.address() as? InetSocketAddress
|
||||
)?.port
|
||||
val response: Response = chain.proceed(request)
|
||||
val t2 = System.nanoTime()
|
||||
|
||||
Log.d("OkHttpLog", "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms")
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
object HttpClientManager {
|
||||
val rootClient =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
|
||||
val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L)
|
||||
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
|
||||
|
||||
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
||||
private var defaultHttpClient: OkHttpClient? = null
|
||||
private var defaultHttpClientWithoutProxy: OkHttpClient? = null
|
||||
private var userAgent: String = "Amethyst"
|
||||
|
||||
private var currentProxy: Proxy? = null
|
||||
|
||||
fun setDefaultProxy(proxy: Proxy?) {
|
||||
if (currentProxy != proxy) {
|
||||
Log.d("HttpClient", "Changing proxy to: ${proxy != null}")
|
||||
this.currentProxy = proxy
|
||||
|
||||
// recreates singleton
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentProxy(): Proxy? = this.currentProxy
|
||||
|
||||
fun setDefaultTimeout(timeout: Duration) {
|
||||
Log.d("HttpClient", "Changing timeout to: $timeout")
|
||||
if (this.defaultTimeout.seconds != timeout.seconds) {
|
||||
this.defaultTimeout = timeout
|
||||
|
||||
// recreates singleton
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultUserAgent(userAgentHeader: String) {
|
||||
Log.d("HttpClient", "Changing userAgent")
|
||||
if (userAgent != userAgentHeader) {
|
||||
this.userAgent = userAgentHeader
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildHttpClient(
|
||||
proxy: Proxy?,
|
||||
timeout: Duration,
|
||||
): OkHttpClient {
|
||||
val seconds = if (proxy != null) timeout.seconds * 3 else timeout.seconds
|
||||
val duration = Duration.ofSeconds(seconds)
|
||||
return rootClient
|
||||
.newBuilder()
|
||||
.proxy(proxy)
|
||||
.readTimeout(duration)
|
||||
.connectTimeout(duration)
|
||||
.writeTimeout(duration)
|
||||
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
|
||||
.addNetworkInterceptor(LoggingInterceptor())
|
||||
.build()
|
||||
}
|
||||
|
||||
class DefaultContentTypeInterceptor(
|
||||
private val userAgentHeader: String,
|
||||
) : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest: Request = chain.request()
|
||||
val requestWithUserAgent: Request =
|
||||
originalRequest
|
||||
.newBuilder()
|
||||
.header("User-Agent", userAgentHeader)
|
||||
.build()
|
||||
return chain.proceed(requestWithUserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentProxyPort(useProxy: Boolean): Int? =
|
||||
if (useProxy) {
|
||||
(currentProxy?.address() as? InetSocketAddress)?.port
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getHttpClient(useProxy: Boolean): OkHttpClient =
|
||||
if (useProxy) {
|
||||
if (this.defaultHttpClient == null) {
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
}
|
||||
defaultHttpClient!!
|
||||
} else {
|
||||
if (this.defaultHttpClientWithoutProxy == null) {
|
||||
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
||||
}
|
||||
defaultHttpClientWithoutProxy!!
|
||||
}
|
||||
|
||||
fun setDefaultProxyOnPort(port: Int) {
|
||||
setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.sockets
|
||||
|
||||
interface WebSocket {
|
||||
fun cancel()
|
||||
|
||||
fun send(msg: String): Boolean
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.sockets
|
||||
|
||||
interface WebSocketListener {
|
||||
fun onOpen(
|
||||
pingMillis: Long,
|
||||
compression: Boolean,
|
||||
)
|
||||
|
||||
fun onMessage(text: String)
|
||||
|
||||
fun onClosing(
|
||||
code: Int,
|
||||
reason: String,
|
||||
)
|
||||
|
||||
fun onClosed(
|
||||
code: Int,
|
||||
reason: String,
|
||||
)
|
||||
|
||||
fun onFailure(
|
||||
t: Throwable,
|
||||
response: String?,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite.sockets
|
||||
|
||||
interface WebsocketBuilder {
|
||||
fun build(
|
||||
url: String,
|
||||
forceProxy: Boolean,
|
||||
out: WebSocketListener,
|
||||
): WebSocket
|
||||
}
|
||||
Reference in New Issue
Block a user