mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Notifications
This commit is contained in:
@ -71,10 +71,14 @@ dependencies {
|
||||
implementation(libs.material)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.kmp.tor)
|
||||
implementation(libs.quartz)
|
||||
implementation(libs.ammolite)
|
||||
implementation(libs.quartz) {
|
||||
exclude("net.java.dev.jna")
|
||||
}
|
||||
implementation(libs.ammolite) {
|
||||
exclude("net.java.dev.jna")
|
||||
}
|
||||
implementation(libs.jna) { artifact { type = "aar" } }
|
||||
implementation(libs.security.crypto.ktx)
|
||||
// Add the KMP Tor binary dependency (contains the native .so files)
|
||||
implementation(libs.kmp.tor.binary)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
|
||||
@ -32,6 +32,8 @@ import com.robosats.models.EncryptedStorage
|
||||
import com.robosats.services.NotificationsService
|
||||
import com.robosats.tor.TorKmp
|
||||
import com.robosats.tor.TorKmpManager
|
||||
import com.robosats.tor.TorKmpManager.getTorKmpObject
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private val requestCodePostNotifications: Int = 1
|
||||
@ -39,6 +41,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var torKmp: TorKmp
|
||||
private lateinit var loadingContainer: ConstraintLayout
|
||||
private lateinit var statusTextView: TextView
|
||||
private lateinit var intentData: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -74,8 +77,27 @@ class MainActivity : AppCompatActivity() {
|
||||
requestCodePostNotifications,
|
||||
)
|
||||
}
|
||||
|
||||
val intent = intent
|
||||
if (intent != null) {
|
||||
val orderId = intent.getStringExtra("order_id")
|
||||
if (orderId?.isNotEmpty() == true) {
|
||||
intentData = orderId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
intent.let {
|
||||
val orderId = intent.getStringExtra("order_id")
|
||||
if (orderId?.isNotEmpty() == true) {
|
||||
intentData = orderId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize Notifications service
|
||||
*/
|
||||
@ -158,6 +180,8 @@ class MainActivity : AppCompatActivity() {
|
||||
runOnUiThread {
|
||||
updateStatus("Tor connected successfully. Setting up secure browser...")
|
||||
|
||||
HttpClientManager.setDefaultProxy(getTorKmpObject().proxy)
|
||||
|
||||
// Now that Tor is connected, set up the WebView
|
||||
setupWebView()
|
||||
}
|
||||
@ -296,6 +320,14 @@ class MainActivity : AppCompatActivity() {
|
||||
webView.loadUrl("file:///android_asset/index.html")
|
||||
|
||||
initializeNotifications()
|
||||
|
||||
webView.post {
|
||||
try {
|
||||
webView.evaluateJavascript("javascript:window.AndroidDataRobosats = { navigateToPage: '$intentData' }", null)
|
||||
} catch (e: Exception) {
|
||||
Log.e("NavigateToPage", "Error evaluating JavaScript: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebViewSetup", "Security error in WebView setup: ${e.message}", e)
|
||||
|
||||
@ -7,6 +7,7 @@ import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import com.robosats.models.EncryptedStorage
|
||||
import com.robosats.models.NostrClient
|
||||
import com.robosats.tor.TorKmpManager.getTorKmpObject
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
@ -166,11 +167,10 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(TAG, "WebSocket opening: " + path)
|
||||
Log.d(TAG, "WebSocket opening: $path")
|
||||
val client: OkHttpClient = Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
|
||||
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
|
||||
.proxy(getTorKmpObject().proxy)
|
||||
.build()
|
||||
|
||||
|
||||
@ -236,7 +236,7 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
||||
return
|
||||
}
|
||||
|
||||
val websocket = webSockets.get(path)
|
||||
val websocket = webSockets[path]
|
||||
if (websocket != null) {
|
||||
websocket.send(message)
|
||||
resolvePromise(uuid, "true")
|
||||
@ -356,6 +356,8 @@ class WebAppInterface(private val context: Context, private val webView: WebView
|
||||
|
||||
EncryptedStorage.setEncryptedStorage(sanitizedKey, sanitizedValue)
|
||||
|
||||
if (key == "federation_relays") NostrClient.refresh()
|
||||
|
||||
// Safely encode and return the result
|
||||
resolvePromise(uuid, key)
|
||||
}
|
||||
|
||||
@ -7,6 +7,9 @@ import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.ammolite.relays.RelayPool
|
||||
import com.vitorpamplona.ammolite.relays.TypedFilter
|
||||
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object NostrClient {
|
||||
@ -25,6 +28,15 @@ object NostrClient {
|
||||
subscribeToInbox()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
val federationRelays = EncryptedStorage.getEncryptedStorage("federation_relays")
|
||||
val relayPool = RelayPool.getAll().map { it.url }
|
||||
if (federationRelays.toSet() != relayPool.toSet()) {
|
||||
stop()
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkRelaysHealth() {
|
||||
if (RelayPool.getAll().isEmpty()) {
|
||||
stop()
|
||||
@ -41,45 +53,101 @@ object NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectRelays() {
|
||||
fun garagePubKeys(): List<String> {
|
||||
val garageString = EncryptedStorage.getEncryptedStorage("garage_slots")
|
||||
var authors = emptyList<String>()
|
||||
|
||||
val garage = JSONObject(garageString)
|
||||
if (garageString.isNotEmpty()) {
|
||||
val garage = JSONObject(garageString)
|
||||
|
||||
val relays = emptyList<String>()
|
||||
val keys = garage.keys()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
val slot = garage.getJSONObject(key) // Get the value associated with the key
|
||||
val hexPubKey = slot.getString("nostrPubKey")
|
||||
if (hexPubKey.isNotEmpty()) {
|
||||
authors = authors.plus(hexPubKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relays.forEach {
|
||||
Client.sendFilterOnlyIfDisconnected()
|
||||
if (RelayPool.getRelays(it).isEmpty()) {
|
||||
RelayPool.addRelay(
|
||||
Relay(
|
||||
it,
|
||||
read = true,
|
||||
write = false,
|
||||
forceProxy = false,
|
||||
activeTypes = COMMON_FEED_TYPES,
|
||||
),
|
||||
)
|
||||
return authors
|
||||
}
|
||||
|
||||
fun getRobotKeyPair(hexPubKey: String): KeyPair {
|
||||
val garageString = EncryptedStorage.getEncryptedStorage("garage_slots")
|
||||
var privKey = ""
|
||||
var pubKey = ""
|
||||
|
||||
if (garageString.isNotEmpty()) {
|
||||
val garage = JSONObject(garageString)
|
||||
|
||||
val keys = garage.keys()
|
||||
while (keys.hasNext() && privKey == "") {
|
||||
val key = keys.next()
|
||||
val slot = garage.getJSONObject(key)
|
||||
val slotPubKey = slot.getString("nostrPubKey")
|
||||
if (slotPubKey == hexPubKey) {
|
||||
pubKey = slotPubKey
|
||||
val nostrSecKeyJson = slot.getJSONObject("nostrSecKey")
|
||||
val byteArray = ByteArray(nostrSecKeyJson.length()) { index ->
|
||||
nostrSecKeyJson.getInt(index.toString()).toByte()
|
||||
}
|
||||
privKey = byteArray.joinToString("") { byte -> "%02x".format(byte) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return KeyPair(
|
||||
Hex.decode(privKey),
|
||||
Hex.decode(pubKey),
|
||||
)
|
||||
}
|
||||
|
||||
private fun connectRelays() {
|
||||
val federationRelays = EncryptedStorage.getEncryptedStorage("federation_relays")
|
||||
|
||||
if (federationRelays.isNotEmpty()) {
|
||||
val relaysUrls = JSONArray(federationRelays)
|
||||
|
||||
for (i in 0 until relaysUrls.length()) {
|
||||
val url = relaysUrls.getString(i)
|
||||
Client.sendFilterOnlyIfDisconnected()
|
||||
if (RelayPool.getRelays(url).isEmpty()) {
|
||||
RelayPool.addRelay(
|
||||
Relay(
|
||||
url,
|
||||
read = true,
|
||||
write = false,
|
||||
forceProxy = true,
|
||||
activeTypes = COMMON_FEED_TYPES,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToInbox() {
|
||||
val authors = emptyList<String>()
|
||||
val garageString = EncryptedStorage.getEncryptedStorage("garage_slots")
|
||||
|
||||
if (authors.isNotEmpty()) {
|
||||
Client.sendFilter(
|
||||
subscriptionNotificationId,
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = SincePerRelayFilter(
|
||||
kinds = listOf(1, 4, 6, 7, 9735),
|
||||
tags = mapOf("p" to authors),
|
||||
if (garageString.isNotEmpty()) {
|
||||
val authors = garagePubKeys()
|
||||
|
||||
if (authors.isNotEmpty()) {
|
||||
Client.sendFilter(
|
||||
subscriptionNotificationId,
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = SincePerRelayFilter(
|
||||
kinds = listOf(1059),
|
||||
tags = mapOf("p" to authors),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,20 @@ package com.robosats.services
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
@ -16,18 +24,29 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.robosats.Connectivity
|
||||
import com.robosats.R
|
||||
import com.robosats.RoboIdentities
|
||||
import com.robosats.models.EncryptedStorage
|
||||
import com.robosats.models.NostrClient
|
||||
import com.robosats.models.NostrClient.garagePubKeys
|
||||
import com.robosats.models.NostrClient.getRobotKeyPair
|
||||
import com.vitorpamplona.ammolite.relays.Client
|
||||
import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import androidx.core.graphics.createBitmap
|
||||
import com.robosats.MainActivity
|
||||
|
||||
class NotificationsService : Service() {
|
||||
private var channelRelaysId = "RelaysConnections"
|
||||
@ -35,6 +54,7 @@ class NotificationsService : Service() {
|
||||
|
||||
private lateinit var notificationGroup: NotificationChannelGroupCompat
|
||||
|
||||
private val roboIdentities = RoboIdentities()
|
||||
private val timer = Timer()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val processedEvents = ConcurrentHashMap<String, Boolean>()
|
||||
@ -47,12 +67,24 @@ class NotificationsService : Service() {
|
||||
relay: Relay,
|
||||
afterEOSE: Boolean,
|
||||
) {
|
||||
if (processedEvents.putIfAbsent(event.id, true) == null) {
|
||||
if (event is GiftWrapEvent && processedEvents.putIfAbsent(event.id, true) == null) {
|
||||
Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId - ${event.toJson()}")
|
||||
val notify = true
|
||||
val firstTaggedUser = event.firstTaggedUser()
|
||||
val authors = garagePubKeys()
|
||||
|
||||
if (notify) {
|
||||
Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId - Broadcast")
|
||||
if (firstTaggedUser?.isNotEmpty() == true && authors.contains(firstTaggedUser)) {
|
||||
Log.d("RobosatsNotifications", "Relay Event: ${relay.url} - $subscriptionId")
|
||||
|
||||
var nostrSigner = NostrSignerInternal(getRobotKeyPair(firstTaggedUser))
|
||||
event.unwrap(nostrSigner) { gift ->
|
||||
if (gift is SealedGossipEvent) {
|
||||
gift.unseal(nostrSigner) { rumor ->
|
||||
if (rumor is ChatMessageEvent) {
|
||||
displayOrderNotification(rumor, firstTaggedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,4 +243,74 @@ class NotificationsService : Service() {
|
||||
notificationManager.notify(1, build)
|
||||
return build
|
||||
}
|
||||
|
||||
private fun displayOrderNotification(event: ChatMessageEvent, hexPubKey: String) {
|
||||
val notificationManager =
|
||||
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val orderId = event.firstTag("order_id")
|
||||
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
putExtra("order_id", orderId)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val garageString = EncryptedStorage.getEncryptedStorage("garage_slots")
|
||||
var bitmap: Bitmap? = null
|
||||
|
||||
if (garageString.isNotEmpty()) {
|
||||
val garage = JSONObject(garageString)
|
||||
val keys = garage.keys()
|
||||
while (keys.hasNext() && bitmap == null) {
|
||||
val key = keys.next()
|
||||
val slot = garage.getJSONObject(key)
|
||||
val slotHexPubKey = slot.getString("nostrPubKey")
|
||||
if (hexPubKey.isNotEmpty() && hexPubKey == slotHexPubKey) {
|
||||
val hashId = slot.getString("hashId")
|
||||
if (hashId.isNotEmpty()) {
|
||||
val base64Image = roboIdentities.generateRobohash("$hashId;80")
|
||||
val imageBytes = Base64.decode(base64Image, Base64.DEFAULT)
|
||||
val rawBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
bitmap = getRoundedBitmap(rawBitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
applicationContext,
|
||||
channelNotificationsId,
|
||||
)
|
||||
.setContentTitle(orderId?.replace("/", "#")?.replaceFirstChar { it.uppercase() })
|
||||
.setContentText(event.content)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(bitmap)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
|
||||
notificationManager.notify(event.id.hashCode(), builder.build())
|
||||
}
|
||||
|
||||
private fun getRoundedBitmap(bitmap: Bitmap): Bitmap {
|
||||
val output = createBitmap(bitmap.width, bitmap.height)
|
||||
val canvas = Canvas(output)
|
||||
val paint = Paint()
|
||||
val path = Path()
|
||||
|
||||
// Create a rounded rectangle path
|
||||
path.addRoundRect(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat(),
|
||||
bitmap.width / 2f, bitmap.height / 2f, Path.Direction.CW)
|
||||
|
||||
canvas.clipPath(path)
|
||||
canvas.drawBitmap(bitmap, 0f, 0f, paint)
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ activity = "1.10.1"
|
||||
constraintlayout = "2.2.1"
|
||||
kmpTor= "4.8.10-0-1.4.5"
|
||||
kmpTorBinary= "4.8.10-0"
|
||||
jna = "5.14.0"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
quartz = "0.92.7"
|
||||
|
||||
@ -30,6 +31,7 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
|
||||
quartz = { module = "com.github.vitorpamplona.amethyst:quartz", version.ref = "quartz" }
|
||||
ammolite = { module = "com.github.vitorpamplona.amethyst:ammolite", version.ref = "quartz" }
|
||||
security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@ -12,18 +12,19 @@ const Routes: React.FC = () => {
|
||||
const { page, navigateToPage } = useContext<UseAppStoreType>(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('navigateToPage', (event) => {
|
||||
const orderId: string = event?.order_id;
|
||||
const coordinator: string = event?.coordinator;
|
||||
if (window.AndroidDataRobosats && garage.currentSlot) {
|
||||
const orderPath = window.AndroidDataRobosats.navigateToPage ?? '';
|
||||
const [coordinator, orderId] = orderPath.replace('#', '/').split('/');
|
||||
window.AndroidDataRobosats = undefined;
|
||||
|
||||
if (orderId && coordinator) {
|
||||
const slot = garage.getSlotByOrder(coordinator, parseInt(orderId, 10));
|
||||
if (slot?.token) {
|
||||
garage.setCurrentSlot(slot?.token);
|
||||
navigateToPage(`order/${coordinator}/${orderId}`, navigate);
|
||||
}
|
||||
if (slot?.token) garage.setCurrentSlot(slot.token);
|
||||
|
||||
navigateToPage(`order/${coordinator}/${orderId}`, navigate);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
}, [garage.currentSlot]);
|
||||
|
||||
useEffect(() => {
|
||||
// change tab (page) into the current route
|
||||
|
||||
@ -9,7 +9,7 @@ const SelfhostedAlert = (): React.JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
systemClient.getItem('selfhosted-alert').then((result) => {
|
||||
if (result) setShow(true);
|
||||
if (!result) setShow(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ const UnsafeAlert = (): React.JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
systemClient.getItem('unsafe-alert').then((result) => {
|
||||
if (result) setShow(true);
|
||||
if (!result) setShow(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ const EncryptedChat: React.FC<Props> = ({
|
||||
try {
|
||||
const recipient = {
|
||||
publicKey,
|
||||
relayUrl: coordinator.getRelayUrl(coordinator.url),
|
||||
relayUrl: coordinator.getRelayUrl(),
|
||||
};
|
||||
|
||||
const wrappedEvent = nip17.wrapEvent(slot?.nostrSecKey, recipient, content);
|
||||
|
||||
@ -137,11 +137,11 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): React
|
||||
initialAppContext.acknowledgedWarning,
|
||||
);
|
||||
|
||||
const navigateToPage: (newPage: Page, navigate: NavigateFunction) => void = (
|
||||
const navigateToPage: (newPage: Page | string, navigate: NavigateFunction) => void = (
|
||||
newPage,
|
||||
navigate,
|
||||
) => {
|
||||
const pathPage: Page | string = newPage.split('/')[0];
|
||||
const pathPage: Page | string = newPage.replace('#', '/').split('/')[0];
|
||||
if (isPage(pathPage)) {
|
||||
setPage(pathPage);
|
||||
navigate(`/${newPage}`);
|
||||
|
||||
@ -281,8 +281,8 @@ export class Coordinator {
|
||||
this.book = {};
|
||||
};
|
||||
|
||||
getRelayUrl = (hostUrl: string): string => {
|
||||
const protocol = hostUrl.includes('https') ? 'wss://' : 'ws://';
|
||||
getRelayUrl = (): string => {
|
||||
const protocol = this.url.includes('https') ? 'wss://' : 'ws://';
|
||||
return this.url.replace(/^https?:\/\//, protocol) + '/relay/';
|
||||
};
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import { coordinatorDefaultValues } from './Coordinator.model';
|
||||
import { updateExchangeInfo } from './Exchange.model';
|
||||
import eventToPublicOrder from '../utils/nostr';
|
||||
import RoboPool from '../services/RoboPool';
|
||||
import { systemClient } from '../services/System';
|
||||
|
||||
type FederationHooks = 'onFederationUpdate';
|
||||
|
||||
@ -61,6 +62,11 @@ export class Federation {
|
||||
if (tesnetHost) this.network = 'testnet';
|
||||
this.connection = null;
|
||||
this.roboPool = new RoboPool(settings);
|
||||
|
||||
if (settings.client === 'mobile') {
|
||||
const federationUrls = Object.values(this.coordinators).map((c) => c.getRelayUrl());
|
||||
systemClient.setItem('federation_relays', JSON.stringify(federationUrls));
|
||||
}
|
||||
}
|
||||
|
||||
private coordinators: Record<string, Coordinator>;
|
||||
|
||||
@ -42,12 +42,14 @@ class Garage {
|
||||
|
||||
save = (): void => {
|
||||
systemClient.setItem('garage_slots', JSON.stringify(this.slots));
|
||||
if (this.currentSlot) systemClient.setItem('garage_current_slot', this.currentSlot);
|
||||
};
|
||||
|
||||
delete = (): void => {
|
||||
this.slots = {};
|
||||
this.currentSlot = null;
|
||||
systemClient.deleteItem('garage_slots');
|
||||
systemClient.deleteItem('garage_current_slot');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
};
|
||||
|
||||
@ -73,9 +75,11 @@ class Garage {
|
||||
);
|
||||
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder));
|
||||
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder));
|
||||
this.currentSlot = rawSlot?.token;
|
||||
}
|
||||
});
|
||||
|
||||
this.currentSlot =
|
||||
(await systemClient.getItem('garage_current_slot')) ?? Object.keys(rawSlots)[0];
|
||||
console.log('Robot Garage was loaded from local storage');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
AndroidDataRobosats?: AndroidDataRobosats;
|
||||
AndroidAppRobosats?: AndroidAppRobosats;
|
||||
AndroidRobosats?: AndroidRobosats;
|
||||
RobosatsSettings: 'web-basic' | 'web-pro' | 'selfhosted-basic' | 'selfhosted-pro';
|
||||
}
|
||||
}
|
||||
|
||||
interface AndroidDataRobosats {
|
||||
navigateToPage: string;
|
||||
}
|
||||
|
||||
interface AndroidAppRobosats {
|
||||
getEncryptedStorage: (uuid: string, key: string) => void;
|
||||
setEncryptedStorage: (uuid: string, key: string, value: string) => void;
|
||||
@ -38,6 +43,10 @@ class AndroidRobosats {
|
||||
}
|
||||
> = {};
|
||||
|
||||
public navigateToPage: (orderPath: string) => void = (orderPath) => {
|
||||
console.log('orderPath', orderPath);
|
||||
};
|
||||
|
||||
public storePromise: (
|
||||
uuid: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -86,15 +95,15 @@ class AndroidRobosats {
|
||||
};
|
||||
|
||||
public onWSMessage: (path: string, message: string) => void = (path, message) => {
|
||||
this.WSConnections[path](message);
|
||||
this.WSConnections[path]?.(message);
|
||||
};
|
||||
|
||||
public onWsError: (path: string) => void = (path) => {
|
||||
this.WSError[path]();
|
||||
this.WSError[path]?.();
|
||||
};
|
||||
|
||||
public onWsClose: (path: string) => void = (path) => {
|
||||
this.WSClose[path]();
|
||||
this.WSClose[path]?.();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@ class RoboPool {
|
||||
updateRelays = (hostUrl: string, coordinators: Coordinator[]) => {
|
||||
this.close();
|
||||
this.relays = [];
|
||||
const federationRelays = coordinators.map((coord) => coord.getRelayUrl(hostUrl));
|
||||
// const hostRelay = federationRelays.find((relay) => relay.includes(hostUrl));
|
||||
const hostRelay = 'ws://45gzfolhp3dcfv6w7a4p2iwekvurdjcf4p2onhnmvyhauwxfsx7kguad.onion/relay/';
|
||||
const federationRelays = coordinators.map((coord) => coord.getRelayUrl());
|
||||
const hostRelay = federationRelays.find((relay) => relay.includes(hostUrl));
|
||||
|
||||
if (hostRelay) this.relays.push(hostRelay);
|
||||
|
||||
while (this.relays.length < 3) {
|
||||
|
||||
@ -279,5 +279,51 @@
|
||||
"mainnetNodesPubkeys": ["028059662c65626f3002290dd3151186bc1952726c03f56012406f3ceadb81cacc"],
|
||||
"testnetNodesPubkeys": [],
|
||||
"federated": true
|
||||
},
|
||||
"satstralia": {
|
||||
"longAlias": "Satstralia",
|
||||
"shortAlias": "satstralia",
|
||||
"identifier": "satstralia",
|
||||
"description": "A Decentralized, deflationary currency enthusiast. Hoping to add liquidity to a non-KYC means for purchasing and selling bitcoin. Robosats P2P solves many issues with the modern day fiat system, and FreedomSats is happy to aid in its decentralization. The focus of FreedomSats will be to ensure data privacy and uptime-consistency.",
|
||||
"motto": "Liberty’s Network Above",
|
||||
"color": "#5d2365",
|
||||
"established": "2025-06-30",
|
||||
"nostrHexPubkey": "116bf3493edd2136f5e0c6a11b6c24d0eb34bbd5bf8ede965a9548726de710c7",
|
||||
"contact": {
|
||||
"email": "FreedomSatoshis@proton.me",
|
||||
"telegram": "@freedomsats",
|
||||
"simplex": "https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fh--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw%3D%40smp15.simplex.im%2FLjWY29_NGy7vujmzRCmnJocepWWMAk2D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAWRfXNPT2xQihjv0Ef2sV51J2YiAmboLcmE0adlEAv2c%253D%26q%3Dc%26srv%3Doauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
|
||||
"pgp": "/static/federation/pgp/9D3D3BAF4744305EE3F0B837331AB575DD78D930.asc",
|
||||
"nostr": "npub1mmfacq4p4xmpeevaz86fv5uuk073tuqry6sk73l9lrtkh2azf0dsykwxuj",
|
||||
"website": null,
|
||||
"fingerprint": "9D3D3BAF4744305EE3F0B837331AB575DD78D930"
|
||||
},
|
||||
"badges": {
|
||||
"isFounder": false,
|
||||
"donatesToDevFund": 20,
|
||||
"hasGoodOpSec": true,
|
||||
"hasLargeLimits": false
|
||||
},
|
||||
"policies": {
|
||||
"Privacy Policy 1": "Dispute Evidence Submission: If a dispute arises, you will be asked to provide only the transaction-related documentation needed to evaluate your claim—for example, transaction IDs, payment‐confirmation screenshots, or other pertinent records. Please keep a record of all relevant information.",
|
||||
"Privacy Policy 2": "Limited Retention Period: All sensitive data gathered for the purpose of resolving a dispute will be retained strictly for the duration of that process. Once the issue is fully resolved, this information will be irrevocably and securely deleted",
|
||||
"Data Policy 1": "No Third-Party Data Sharing: RoboSats is deliberately designed to collect no user data—no IP addresses, geolocation, or personal identifiers—and will never share information with outside parties.",
|
||||
"Data Policy 2": "Ephemeral Log Retention: All operational logs required to run the coordinator are automatically deleted after seven days, unless they’re essential for an active dispute.",
|
||||
"Rule 1:": "Please do not share any personal information through the chat, if possible share as little revealing information as possible.",
|
||||
"Rule 2:": "Email is provided for convenience but other means of communication are recommended. Like Nostr/SimpleX/Telegram"
|
||||
},
|
||||
"mainnet": {
|
||||
"onion": "http://45gzfolhp3dcfv6w7a4p2iwekvurdjcf4p2onhnmvyhauwxfsx7kguad.onion",
|
||||
"clearnet": null,
|
||||
"i2p": null
|
||||
},
|
||||
"testnet": {
|
||||
"onion": null,
|
||||
"clearnet": null,
|
||||
"i2p": null
|
||||
},
|
||||
"mainnetNodesPubkeys": ["028059662c65626f3002290dd3151186bc1952726c03f56012406f3ceadb81cacc"],
|
||||
"testnetNodesPubkeys": [],
|
||||
"federated": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user