mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
Geohash-specific relays (#306)
* per-relay-chat * normalize relay URL * turn off default relays * add relay list * fetch relay list from the internet
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
Relay URL,Latitude,Longitude
|
||||
relay.damus.io,37.7621,-122.3971
|
||||
nostr-pub.wellorder.net,45.5229,-122.9898
|
||||
nostr.mom,50.4779,12.3713
|
||||
nostr.slothy.win,37.7621,-122.3971
|
||||
nostr.einundzwanzig.space,50.1155,8.6842
|
||||
nos.lol,50.4779,12.3713
|
||||
relay.nostr.band,60.1695,24.9354
|
||||
no.str.cr,9.9339,-84.0849
|
||||
nostr.massmux.com,50.1155,8.6842
|
||||
nostr-relay.schnitzel.world,39.0437,-77.4875
|
||||
relay.nostr.com.au,37.7621,-122.3971
|
||||
knostr.neutrine.com,48.8534,2.3488
|
||||
nostr.nodeofsven.com,47.4875,8.2965
|
||||
nostr.vulpem.com,49.4542,11.0775
|
||||
nostr-verif.slothy.win,37.7621,-122.3971
|
||||
relay.lexingtonbitcoin.org,37.7621,-122.3971
|
||||
nostr-1.nbo.angani.co,-1.2615,36.7903
|
||||
relay.wellorder.net,45.5229,-122.9898
|
||||
nostr.easydns.ca,43.7064,-79.3986
|
||||
relay.dwadziesciajeden.pl,52.2298,21.0118
|
||||
nostr.data.haus,50.4779,12.3713
|
||||
nostr.einundzwanzig.space,50.1155,8.6842
|
||||
nostr.mom,50.4779,12.3713
|
||||
nos.lol,50.4779,12.3713
|
||||
relay.nostr.band,60.1695,24.9354
|
||||
nostr.massmux.com,50.1155,8.6842
|
||||
nostr.vulpem.com,49.4542,11.0775
|
||||
relay.damus.io,37.7621,-122.3971
|
||||
nostr-pub.wellorder.net,45.5229,-122.9898
|
||||
no.str.cr,9.9339,-84.0849
|
||||
relay.dwadziesciajeden.pl,52.2298,21.0118
|
||||
nostr.data.haus,50.4779,12.3713
|
||||
relay.wellorder.net,45.5229,-122.9898
|
||||
relay.nostromo.social,49.4542,11.0775
|
||||
offchain.pub,34.0522,-118.2437
|
||||
relay.nostr.wirednet.jp,35.9356,139.3044
|
||||
relay.nostrcheck.me,37.7621,-122.3971
|
||||
nostrue.com,40.8043,-74.0121
|
||||
nostr-relay.schnitzel.world,39.0437,-77.4875
|
||||
nproxy.kristapsk.lv,60.1695,24.9354
|
||||
nostr.spaceshell.xyz,37.7621,-122.3971
|
||||
nostr-dev.wellorder.net,45.5229,-122.9898
|
||||
nostr-verified.wellorder.net,45.5229,-122.9898
|
||||
nostr.roundrockbitcoiners.com,40.8043,-74.0121
|
||||
slick.mjex.me,39.0437,-77.4875
|
||||
nostr.yael.at,52.3740,4.8897
|
||||
relay.primal.net,37.7621,-122.3971
|
||||
nostr.oxtr.dev,50.4779,12.3713
|
||||
nostr.21crypto.ch,46.5160,6.6328
|
||||
nostr.liberty.fans,38.8003,-90.6265
|
||||
nostr-02.dorafactory.org,1.2897,103.8501
|
||||
relay.hodl.ar,-32.9468,-60.6393
|
||||
nostr.middling.mydns.jp,35.8089,140.1185
|
||||
nostr.namek.link,37.7621,-122.3971
|
||||
nostrja-kari.heguro.com,37.7621,-122.3971
|
||||
nostr.hifish.org,47.3667,8.5500
|
||||
nostr.rikmeijer.nl,50.4779,12.3713
|
||||
black.nostrcity.club,41.8119,-87.6873
|
||||
nostr.hekster.org,37.3924,-121.9623
|
||||
relay.wavlake.com,41.2619,-95.8608
|
||||
nostr.sagaciousd.com,49.2497,-123.1193
|
||||
nostr.fbxl.net,43.7064,-79.3986
|
||||
ithurtswhenip.ee,50.7990,-1.0913
|
||||
relay2.nostrchat.io,49.4542,11.0775
|
||||
relay1.nostrchat.io,60.1695,24.9354
|
||||
nostr-01.yakihonne.com,1.3215,103.6957
|
||||
nostr.sathoarder.com,48.5839,7.7455
|
||||
nostr.overmind.lol,37.7621,-122.3971
|
||||
relay.verified-nostr.com,37.7621,-122.3971
|
||||
purplerelay.com,50.1155,8.6842
|
||||
relay.orangepill.ovh,49.0127,1.9694
|
||||
nostr-relay.psfoundation.info,39.0437,-77.4875
|
||||
soloco.nl,37.7621,-122.3971
|
||||
relay.froth.zone,60.1695,24.9354
|
||||
nostr.stakey.net,52.5250,5.7181
|
||||
nostr.2b9t.xyz,34.0522,-118.2437
|
||||
pyramid.fiatjaf.com,50.1155,8.6842
|
||||
a.nos.lol,50.4779,12.3713
|
||||
relay.magiccity.live,25.8130,-80.2320
|
||||
nostr.notribe.net,40.8344,-74.1377
|
||||
freelay.sovbit.host,64.1355,-21.8954
|
||||
relay.credenso.cafe,43.4254,-80.5112
|
||||
nostr.huszonegy.world,47.4984,19.0404
|
||||
multiplexer.huszonegy.world,47.4984,19.0404
|
||||
bucket.coracle.social,37.7621,-122.3971
|
||||
nostr.kungfu-g.rip,33.7865,-84.4454
|
||||
relay.artx.market,43.7064,-79.3986
|
||||
relay.notoshi.win,13.3622,100.9835
|
||||
vitor.nostr1.com,40.7143,-74.0060
|
||||
nostr-02.yakihonne.com,1.3215,103.6957
|
||||
nostr-03.dorafactory.org,1.2897,103.8501
|
||||
n.ok0.org,-36.8485,174.7635
|
||||
nostr.0x7e.xyz,47.5056,8.7241
|
||||
relay.nostr.net,50.4779,12.3713
|
||||
strfry.openhoofd.nl,51.5717,3.7042
|
||||
relay.fountain.fm,39.0997,-94.5786
|
||||
relay.usefusion.ai,38.7135,-78.1594
|
||||
relay.varke.eu,52.6958,6.1944
|
||||
nostr.satstralia.com,64.1355,-21.8954
|
||||
relay.13room.space,37.7621,-122.3971
|
||||
nostr.myshosholoza.co.za,52.3710,4.9042
|
||||
nostr.carroarmato0.be,50.8517,3.6089
|
||||
nostr.dbtc.link,37.7621,-122.3971
|
||||
orangepiller.org,60.1695,24.9354
|
||||
adre.su,59.9386,30.3141
|
||||
relay.sincensura.org,37.7621,-122.3971
|
||||
relay.freeplace.nl,52.3740,4.8897
|
||||
bostr.bitcointxoko.com,64.1355,-21.8954
|
||||
nostr.plantroon.com,50.1025,8.6299
|
||||
srtrelay.c-stellar.net,37.7621,-122.3971
|
||||
nostr.jfischer.org,49.4453,11.0222
|
||||
nostr.novacisko.cz,52.2298,21.0118
|
||||
relay.lumina.rocks,49.4453,11.0222
|
||||
nostr.tavux.tech,50.9519,1.8563
|
||||
relay.nostrhub.fr,50.1155,8.6842
|
||||
relay.agorist.space,52.3740,4.8897
|
||||
chorus.pjv.me,45.5229,-122.9898
|
||||
relay.cosmicbolt.net,37.3924,-121.9623
|
||||
santo.iguanatech.net,40.8344,-74.1377
|
||||
relay.tagayasu.xyz,45.4112,-75.6981
|
||||
relay.mostro.network,40.8344,-74.1377
|
||||
relay.zone667.com,60.1695,24.9354
|
||||
relay5.bitransfer.org,37.7621,-122.3971
|
||||
relay.illuminodes.com,47.6062,-122.3321
|
||||
relay2.angor.io,50.1155,8.6842
|
||||
relay.satsdays.com,1.2897,103.8501
|
||||
relay.angor.io,50.1155,8.6842
|
||||
orangesync.tech,50.9333,6.9500
|
||||
nostr-relay.cbrx.io,37.7621,-122.3971
|
||||
relay.21e6.cz,50.0880,14.4208
|
||||
nostr.chaima.info,50.1155,8.6842
|
||||
relay.satlantis.io,32.8546,-79.9748
|
||||
relay.digitalezukunft.cyou,45.5088,-73.5878
|
||||
relay.tapestry.ninja,40.8043,-74.0121
|
||||
relay.minibolt.info,37.7621,-122.3971
|
||||
nostr.bilthon.dev,25.8130,-80.2320
|
||||
nostr.makibisskey.work,37.7621,-122.3971
|
||||
relay.mattybs.lol,37.7621,-122.3971
|
||||
noxir.kpherox.dev,34.8436,135.5084
|
||||
sendit.nosflare.com,37.7621,-122.3971
|
||||
relay.coinos.io,37.7621,-122.3971
|
||||
relay.nostraddress.com,37.7621,-122.3971
|
||||
wot.nostr.party,36.1659,-86.7844
|
||||
nostrelites.org,41.8500,-87.6500
|
||||
relay.nostriot.com,43.7064,-79.3986
|
||||
prl.plus,55.7522,37.6156
|
||||
zap.watch,45.5088,-73.5878
|
||||
wot.codingarena.top,50.4779,12.3713
|
||||
nostr.azzamo.net,52.2284,21.0522
|
||||
wot.sudocarlos.com,43.7064,-79.3986
|
||||
relay.lnfi.network,45.6241,8.7851
|
||||
wot.nostr.net,37.7621,-122.3971
|
||||
relay.nostrdice.com,-33.8678,151.2073
|
||||
wot.sebastix.social,51.3700,6.1681
|
||||
wheat.happytavern.co,37.7621,-122.3971
|
||||
relay.sigit.io,50.4779,12.3713
|
||||
strfry.bonsai.com,37.8716,-122.2728
|
||||
travis-shears-nostr-relay-v2.fly.dev,41.8119,-87.6873
|
||||
satsage.xyz,37.3924,-121.9623
|
||||
relay.degmods.com,50.4779,12.3713
|
||||
nostr.community.ath.cx,45.5088,-73.5878
|
||||
nostr.coincrowd.fund,39.0437,-77.4875
|
||||
strfry.shock.network,41.8847,-88.2040
|
||||
cyberspace.nostr1.com,40.7143,-74.0060
|
||||
relay02.lnfi.network,39.0997,-94.5786
|
||||
nostr-rs-relay.dev.fedibtc.com,39.0437,-77.4875
|
||||
relay.davidebtc.me,50.1155,8.6842
|
||||
wot.dtonon.com,37.7621,-122.3971
|
||||
relay.goodmorningbitcoin.com,37.7621,-122.3971
|
||||
articles.layer3.news,37.3394,-121.8950
|
||||
bostr.syobon.net,37.7621,-122.3971
|
||||
nostr.agentcampfire.com,52.3740,4.8897
|
||||
nostr.thebiglake.org,32.7244,-96.6755
|
||||
schnorr.me,37.7621,-122.3971
|
||||
relay.wolfcoil.com,35.6090,139.7302
|
||||
nostr.camalolo.com,24.1469,120.6839
|
||||
nostr.tac.lol,47.4740,-122.2610
|
||||
dev-relay.lnfi.network,39.0997,-94.5786
|
||||
relay.bitcoinveneto.org,64.1355,-21.8954
|
||||
nostr.red5d.dev,37.7621,-122.3971
|
||||
relay-testnet.k8s.layer3.news,37.3394,-121.8950
|
||||
promenade.fiatjaf.com,50.1155,8.6842
|
||||
nostrelay.memory-art.xyz,37.7621,-122.3971
|
||||
inbox.azzamo.net,52.2284,21.0522
|
||||
social.proxymana.net,60.1695,24.9354
|
||||
relay.netstr.io,53.3331,-6.2489
|
||||
premium.primal.net,37.7621,-122.3971
|
||||
nostr.lojong.info,37.7621,-122.3971
|
||||
nostr-rs-relay-ishosta.phamthanh.me,37.7621,-122.3971
|
||||
relay.stream.labs.h3.se,59.3294,18.0687
|
||||
tollbooth.stens.dev,51.4566,7.0123
|
||||
relay.chakany.systems,37.7621,-122.3971
|
||||
relay.mwaters.net,50.9519,1.8563
|
||||
nostr-relay.shirogaku.xyz,37.7621,-122.3971
|
||||
kitchen.zap.cooking,37.7621,-122.3971
|
||||
relay.arx-ccn.com,50.4779,12.3713
|
||||
relay.fr13nd5.com,50.1155,8.6842
|
||||
nostr.tegila.com.br,39.0437,-77.4875
|
||||
relay.jeffg.fyi,43.7064,-79.3986
|
||||
relay.bullishbounty.com,37.7621,-122.3971
|
||||
nostr.spicyz.io,37.7621,-122.3971
|
||||
relay04.lnfi.network,39.0997,-94.5786
|
||||
vidono.apps.slidestr.net,48.8534,2.3488
|
||||
relay03.lnfi.network,39.0997,-94.5786
|
||||
communities.nos.social,40.8344,-74.1377
|
||||
relay.evanverma.com,40.8344,-74.1377
|
||||
nostrelay.circum.space,51.4566,7.0123
|
||||
wot.brightbolt.net,47.6928,-116.7850
|
||||
relayrs.notoshi.win,37.7621,-122.3971
|
||||
fenrir-s.notoshi.win,37.7621,-122.3971
|
||||
relay.nsnip.io,60.1695,24.9354
|
||||
x.kojira.io,37.7621,-122.3971
|
||||
relay.hasenpfeffr.com,39.0437,-77.4875
|
||||
relay01.lnfi.network,39.0997,-94.5786
|
||||
nostr.rtvslawenia.com,49.4542,11.0775
|
||||
relay.g1sms.fr,43.9298,2.1480
|
||||
nostr.kalf.org,52.3740,4.8897
|
||||
nostr.rblb.it,37.7621,-122.3971
|
||||
nostr.4rs.nl,49.4453,11.0222
|
||||
relay.vrtmrz.net,37.7621,-122.3971
|
||||
nostr.hoppe-relay.it.com,45.5946,-121.1787
|
||||
relay-rpi.edufeed.org,49.4453,11.0222
|
||||
relay.copylaradio.com,50.7990,-1.0913
|
||||
relay.ru.ac.th,13.7540,100.5014
|
||||
relay.bitcoinartclock.com,50.4779,12.3713
|
||||
wot.downisontheup.ca,47.6062,-122.3321
|
||||
nostr.coincards.com,43.7064,-79.3986
|
||||
relay.etch.social,41.2619,-95.8608
|
||||
relay.mess.ch,47.1345,9.0964
|
||||
relay.holzeis.me,37.7621,-122.3971
|
||||
relay-admin.thaliyal.com,40.8220,-74.4488
|
||||
nostr.thaliyal.com,40.8220,-74.4488
|
||||
strfry.felixzieger.de,50.1025,8.6299
|
||||
nostr.smut.cloud,37.7621,-122.3971
|
||||
r.bitcoinhold.net,37.7621,-122.3971
|
||||
nostr.blankfors.se,60.1695,24.9354
|
||||
portal-relay.pareto.space,49.4453,11.0222
|
||||
relay.getsafebox.app,43.7064,-79.3986
|
||||
relay.anzenkodo.workers.dev,37.7621,-122.3971
|
||||
relay.nostrhub.tech,49.4453,11.0222
|
||||
nostr.prl.plus,52.3740,4.8897
|
||||
nostr-2.21crypto.ch,46.5160,6.6328
|
||||
nostr.zenon.network,40.7143,-74.0060
|
||||
nostr-relay.amethyst.name,35.7721,-78.6386
|
||||
relayone.geektank.ai,17.1210,-61.8433
|
||||
fanfares.nostr1.com,40.7143,-74.0060
|
||||
wot.geektank.ai,17.1210,-61.8433
|
||||
relay-dev.satlantis.io,40.8344,-74.1377
|
||||
relay.siamdev.cc,13.9178,100.4240
|
||||
relay.nosto.re,51.3700,6.1681
|
||||
wot.soundhsa.com,39.0997,-94.5786
|
||||
nostr.n7ekb.net,47.5707,-122.2221
|
||||
relayone.soundhsa.com,39.0997,-94.5786
|
||||
relay.puresignal.news,37.7621,-122.3971
|
||||
relay.nostx.io,37.7621,-122.3971
|
||||
nostr.now,35.6090,139.7302
|
||||
relay.artiostr.ch,37.7621,-122.3971
|
||||
relay.oldenburg.cool,50.1155,8.6842
|
||||
theoutpost.life,64.1355,-21.8954
|
||||
khatru.nostrver.se,51.3700,6.1681
|
||||
relay.wavefunc.live,37.7915,-122.4018
|
||||
nostr-relay.zimage.com,34.0522,-118.2437
|
||||
relay.javi.space,43.4628,11.8807
|
||||
bostr.shop,42.8865,-78.8784
|
||||
relay.letsfo.com,52.2298,21.0118
|
||||
alien.macneilmediagroup.com,37.7621,-122.3971
|
||||
rn1.sotiras.org,37.7621,-122.3971
|
||||
gnostr.com,42.6975,23.3241
|
||||
relay.conduit.market,37.7621,-122.3971
|
||||
relay.hivetalk.org,37.3924,-121.9623
|
||||
nostr.l484.com,30.2960,-97.6396
|
||||
relay.chorus.community,50.1155,8.6842
|
||||
nostr-relay.moe.gift,37.7621,-122.3971
|
||||
relay.nostrcal.com,37.7621,-122.3971
|
||||
temp.iris.to,37.7621,-122.3971
|
||||
librerelay.aaroniumii.com,37.7621,-122.3971
|
||||
nostr-relay-1.trustlessenterprise.com,37.7621,-122.3971
|
||||
relay.barine.co,37.7621,-122.3971
|
||||
nostr.rohoss.com,48.1374,11.5755
|
||||
wot.nostr.place,30.2672,-97.7431
|
||||
relay.utxo.farm,34.7331,135.8183
|
||||
relay.bankless.at,37.7621,-122.3971
|
||||
relay.toastr.net,40.8043,-74.0121
|
||||
nostr.excentered.com,52.5244,13.4105
|
||||
relay.mccormick.cx,52.3740,4.8897
|
||||
relay.cypherflow.ai,48.8534,2.3488
|
||||
relay.laantungir.net,45.3134,-73.8725
|
||||
nostr.veladan.dev,37.7621,-122.3971
|
||||
nostr.tadryanom.me,37.7621,-122.3971
|
||||
nostr-relay.online,37.7621,-122.3971
|
||||
nostr.night7.space,50.4779,12.3713
|
||||
dev-nostr.bityacht.io,25.0531,121.5264
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.bitchat.android
|
||||
|
||||
import android.app.Application
|
||||
import com.bitchat.android.nostr.RelayDirectory
|
||||
|
||||
/**
|
||||
* Main application class for bitchat Android
|
||||
@@ -10,7 +11,7 @@ class BitchatApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Initialize any global services or configurations
|
||||
// For now, keep it simple
|
||||
// Initialize relay directory (loads assets/nostr_relays.csv)
|
||||
RelayDirectory.initialize(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package com.bitchat.android.geohash
|
||||
object Geohash {
|
||||
|
||||
private val base32Chars = "0123456789bcdefghjkmnpqrstuvwxyz".toCharArray()
|
||||
private val charToValue: Map<Char, Int> = base32Chars.withIndex().associate { it.value to it.index }
|
||||
|
||||
/**
|
||||
* Encodes the provided coordinates into a geohash string.
|
||||
@@ -62,4 +63,42 @@ object Geohash {
|
||||
|
||||
return geohash.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a geohash string to the center latitude/longitude of its cell.
|
||||
* @return Pair(latitude, longitude)
|
||||
*/
|
||||
fun decodeToCenter(geohash: String): Pair<Double, Double> {
|
||||
if (geohash.isEmpty()) return 0.0 to 0.0
|
||||
|
||||
var latInterval = -90.0 to 90.0
|
||||
var lonInterval = -180.0 to 180.0
|
||||
var isEven = true
|
||||
|
||||
geohash.lowercase().forEach { ch ->
|
||||
val cd = charToValue[ch] ?: return 0.0 to 0.0
|
||||
for (mask in intArrayOf(16, 8, 4, 2, 1)) {
|
||||
if (isEven) {
|
||||
val mid = (lonInterval.first + lonInterval.second) / 2
|
||||
if ((cd and mask) != 0) {
|
||||
lonInterval = mid to lonInterval.second
|
||||
} else {
|
||||
lonInterval = lonInterval.first to mid
|
||||
}
|
||||
} else {
|
||||
val mid = (latInterval.first + latInterval.second) / 2
|
||||
if ((cd and mask) != 0) {
|
||||
latInterval = mid to latInterval.second
|
||||
} else {
|
||||
latInterval = latInterval.first to mid
|
||||
}
|
||||
}
|
||||
isEven = !isEven
|
||||
}
|
||||
}
|
||||
|
||||
val latCenter = (latInterval.first + latInterval.second) / 2
|
||||
val lonCenter = (lonInterval.first + lonInterval.second) / 2
|
||||
return latCenter to lonCenter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,12 @@ class NostrGeohashService(
|
||||
)
|
||||
|
||||
val nostrRelayManager = NostrRelayManager.getInstance(application)
|
||||
nostrRelayManager.sendEvent(event)
|
||||
nostrRelayManager.sendEventToGeohash(
|
||||
event = event,
|
||||
geohash = channel.geohash,
|
||||
includeDefaults = false,
|
||||
nRelays = 5
|
||||
)
|
||||
|
||||
Log.i(TAG, "📤 Sent geohash message to ${channel.geohash}: ${content.take(50)}")
|
||||
|
||||
@@ -532,7 +537,7 @@ class NostrGeohashService(
|
||||
geohashSamplingJob = coroutineScope.launch {
|
||||
val nostrRelayManager = NostrRelayManager.getInstance(application)
|
||||
|
||||
// Subscribe to each geohash for ephemeral events (kind 20000)
|
||||
// Subscribe to each geohash for ephemeral events (kind 20000) using geohash-specific relays
|
||||
geohashes.forEach { geohash ->
|
||||
val filter = NostrFilter.geohashEphemeral(
|
||||
geohash = geohash,
|
||||
@@ -540,12 +545,15 @@ class NostrGeohashService(
|
||||
limit = 200
|
||||
)
|
||||
|
||||
nostrRelayManager.subscribe(
|
||||
nostrRelayManager.subscribeForGeohash(
|
||||
geohash = geohash,
|
||||
filter = filter,
|
||||
id = "geohash-$geohash",
|
||||
handler = { event ->
|
||||
handleUnifiedGeohashEvent(event, geohash)
|
||||
}
|
||||
},
|
||||
includeDefaults = false,
|
||||
nRelays = 5
|
||||
)
|
||||
|
||||
Log.d(TAG, "Subscribed to geohash events for: $geohash")
|
||||
@@ -858,7 +866,7 @@ class NostrGeohashService(
|
||||
try {
|
||||
val nostrRelayManager = NostrRelayManager.getInstance(application)
|
||||
|
||||
// Subscribe to geohash ephemeral events for this specific channel
|
||||
// Subscribe to geohash ephemeral events for this specific channel using geohash-specific relays
|
||||
val geohashSubId = "geohash-${channel.channel.geohash}"
|
||||
currentGeohashSubscriptionId = geohashSubId
|
||||
|
||||
@@ -868,12 +876,15 @@ class NostrGeohashService(
|
||||
limit = 200
|
||||
)
|
||||
|
||||
nostrRelayManager.subscribe(
|
||||
nostrRelayManager.subscribeForGeohash(
|
||||
geohash = channel.channel.geohash,
|
||||
filter = geohashFilter,
|
||||
id = geohashSubId,
|
||||
handler = { event ->
|
||||
handleUnifiedGeohashEvent(event, channel.channel.geohash)
|
||||
}
|
||||
},
|
||||
includeDefaults = false,
|
||||
nRelays = 5
|
||||
)
|
||||
|
||||
Log.i(TAG, "✅ Subscribed to geohash ephemeral events: #${channel.channel.geohash}")
|
||||
@@ -892,12 +903,15 @@ class NostrGeohashService(
|
||||
since = System.currentTimeMillis() - 86400000L // Last 24 hours
|
||||
)
|
||||
|
||||
nostrRelayManager.subscribe(
|
||||
nostrRelayManager.subscribeForGeohash(
|
||||
geohash = channel.channel.geohash,
|
||||
filter = dmFilter,
|
||||
id = dmSubId,
|
||||
handler = { giftWrap ->
|
||||
handleGeohashDmEvent(giftWrap, channel.channel.geohash, dmIdentity)
|
||||
}
|
||||
},
|
||||
includeDefaults = false,
|
||||
nRelays = 5
|
||||
)
|
||||
|
||||
Log.i(TAG, "✅ Subscribed to geohash DMs for identity: ${dmIdentity.publicKeyHex.take(16)}...")
|
||||
|
||||
@@ -52,6 +52,8 @@ class NostrRelayManager private constructor() {
|
||||
fun registerPendingGiftWrap(id: String) {
|
||||
pendingGiftWrapIDs.add(id)
|
||||
}
|
||||
|
||||
fun defaultRelays(): List<String> = DEFAULT_RELAYS
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +95,8 @@ class NostrRelayManager private constructor() {
|
||||
val filter: NostrFilter,
|
||||
val handler: (NostrEvent) -> Unit,
|
||||
val targetRelayUrls: Set<String>? = null, // null means all relays
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val originGeohash: String? = null // used for logging and grouping
|
||||
)
|
||||
|
||||
// Event deduplication system
|
||||
@@ -119,6 +122,103 @@ class NostrRelayManager private constructor() {
|
||||
|
||||
private val gson by lazy { NostrRequest.createGson() }
|
||||
|
||||
// Per-geohash relay selection
|
||||
private val geohashToRelays = ConcurrentHashMap<String, Set<String>>() // geohash -> relay URLs
|
||||
|
||||
// --- Public API for geohash-specific operation ---
|
||||
|
||||
/**
|
||||
* Compute and connect to relays for a given geohash (nearest + optional defaults), cache the mapping.
|
||||
*/
|
||||
fun ensureGeohashRelaysConnected(geohash: String, nRelays: Int = 5, includeDefaults: Boolean = false) {
|
||||
try {
|
||||
val nearest = RelayDirectory.closestRelaysForGeohash(geohash, nRelays)
|
||||
val selected = if (includeDefaults) {
|
||||
(nearest + Companion.defaultRelays()).toSet()
|
||||
} else nearest.toSet()
|
||||
if (selected.isEmpty()) {
|
||||
Log.w(TAG, "No relays selected for geohash=$geohash")
|
||||
return
|
||||
}
|
||||
geohashToRelays[geohash] = selected
|
||||
Log.i(TAG, "🌐 Geohash $geohash using ${selected.size} relays: ${selected.joinToString()}")
|
||||
ensureConnectionsFor(selected)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to ensure relays for $geohash: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relays mapped to a geohash (empty list if none configured).
|
||||
*/
|
||||
fun getRelaysForGeohash(geohash: String): List<String> {
|
||||
return geohashToRelays[geohash]?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe with explicit geohash routing; ensures connections exist, then targets only those relays.
|
||||
*/
|
||||
fun subscribeForGeohash(
|
||||
geohash: String,
|
||||
filter: NostrFilter,
|
||||
id: String = generateSubscriptionId(),
|
||||
handler: (NostrEvent) -> Unit,
|
||||
includeDefaults: Boolean = false,
|
||||
nRelays: Int = 5
|
||||
): String {
|
||||
ensureGeohashRelaysConnected(geohash, nRelays, includeDefaults)
|
||||
val relayUrls = getRelaysForGeohash(geohash)
|
||||
Log.d(TAG, "📡 Subscribing id=$id for geohash=$geohash on ${relayUrls.size} relays")
|
||||
return subscribe(
|
||||
filter = filter,
|
||||
id = id,
|
||||
handler = handler,
|
||||
targetRelayUrls = relayUrls
|
||||
).also {
|
||||
// update origin geohash for this subscription
|
||||
activeSubscriptions[it]?.let { sub ->
|
||||
activeSubscriptions[it] = sub.copy(originGeohash = geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event specifically to a geohash's relays (+ optional defaults).
|
||||
*/
|
||||
fun sendEventToGeohash(event: NostrEvent, geohash: String, includeDefaults: Boolean = false, nRelays: Int = 5) {
|
||||
ensureGeohashRelaysConnected(geohash, nRelays, includeDefaults)
|
||||
val relayUrls = getRelaysForGeohash(geohash)
|
||||
if (relayUrls.isEmpty()) {
|
||||
Log.w(TAG, "No target relays to send event for geohash=$geohash; falling back to defaults")
|
||||
sendEvent(event, Companion.defaultRelays())
|
||||
return
|
||||
}
|
||||
Log.v(TAG, "📤 Sending event kind=${event.kind} to ${relayUrls.size} relays for geohash=$geohash")
|
||||
sendEvent(event, relayUrls)
|
||||
}
|
||||
|
||||
// --- Internal helpers ---
|
||||
|
||||
private fun ensureConnectionsFor(relayUrls: Set<String>) {
|
||||
// Ensure relays are tracked for UI/status
|
||||
relayUrls.forEach { url ->
|
||||
if (relaysList.none { it.url == url }) {
|
||||
relaysList.add(Relay(url))
|
||||
}
|
||||
}
|
||||
updateRelaysList()
|
||||
|
||||
scope.launch {
|
||||
relayUrls.forEach { relayUrl ->
|
||||
launch {
|
||||
if (!connections.containsKey(relayUrl)) {
|
||||
connectToRelay(relayUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Initialize with default relays - avoid static initialization order issues
|
||||
try {
|
||||
@@ -548,7 +648,12 @@ class NostrRelayManager private constructor() {
|
||||
val wasProcessed = eventDeduplicator.processEvent(response.event) { event ->
|
||||
// Only log non-gift-wrap events to reduce noise
|
||||
if (event.kind != NostrKind.GIFT_WRAP) {
|
||||
Log.v(TAG, "📥 Processing new Nostr event (kind: ${event.kind}) from relay: $relayUrl")
|
||||
val originGeo = activeSubscriptions[response.subscriptionId]?.originGeohash
|
||||
if (originGeo != null) {
|
||||
Log.v(TAG, "📥 Processing event (kind=${event.kind}) from relay=$relayUrl geo=$originGeo sub=${response.subscriptionId}")
|
||||
} else {
|
||||
Log.v(TAG, "📥 Processing event (kind=${event.kind}) from relay=$relayUrl sub=${response.subscriptionId}")
|
||||
}
|
||||
}
|
||||
|
||||
// Call handler for new events only
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
/**
|
||||
* Loads relay coordinates from assets and provides nearest-relay lookup by geohash.
|
||||
*/
|
||||
object RelayDirectory {
|
||||
|
||||
private const val TAG = "RelayDirectory"
|
||||
private const val ASSET_FILE_URL = "https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv"
|
||||
private const val ASSET_FILE = "nostr_relays.csv"
|
||||
private const val DOWNLOADED_FILE = "nostr_relays_latest.csv"
|
||||
private const val PREFS_NAME = "relay_directory_prefs"
|
||||
private const val KEY_LAST_UPDATE_MS = "last_update_ms"
|
||||
private val ONE_DAY_MS = TimeUnit.DAYS.toMillis(1)
|
||||
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val httpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.callTimeout(15, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
data class RelayInfo(
|
||||
val url: String,
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var initialized: Boolean = false
|
||||
|
||||
private val relays: MutableList<RelayInfo> = mutableListOf()
|
||||
private val relaysLock = Any()
|
||||
|
||||
fun initialize(application: Application) {
|
||||
if (initialized) return
|
||||
synchronized(this) {
|
||||
if (initialized) return
|
||||
try {
|
||||
val downloaded = getDownloadedFile(application)
|
||||
val loadedFromDownloaded = if (downloaded.exists() && downloaded.canRead()) {
|
||||
loadFromFile(downloaded, sourceLabel = "downloaded")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (!loadedFromDownloaded) {
|
||||
loadFromAssets(application)
|
||||
}
|
||||
|
||||
initialized = true
|
||||
|
||||
// Trigger an immediate fetch if the data is stale (older than 24h)
|
||||
ioScope.launch {
|
||||
if (isStale(application)) {
|
||||
fetchAndMaybeSwap(application)
|
||||
}
|
||||
}
|
||||
|
||||
// Start periodic staleness check every minute
|
||||
startPeriodicRefresh(application)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize RelayDirectory: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return up to nRelays closest relay URLs to the geohash center.
|
||||
*/
|
||||
fun closestRelaysForGeohash(geohash: String, nRelays: Int): List<String> {
|
||||
val snapshot = synchronized(relaysLock) { relays.toList() }
|
||||
if (snapshot.isEmpty()) return emptyList()
|
||||
val center = try {
|
||||
val c = com.bitchat.android.geohash.Geohash.decodeToCenter(geohash)
|
||||
c
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decode geohash '$geohash': ${e.message}")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val (lat, lon) = center
|
||||
return snapshot
|
||||
.asSequence()
|
||||
.sortedBy { haversineMeters(lat, lon, it.latitude, it.longitude) }
|
||||
.take(nRelays.coerceAtLeast(0))
|
||||
.map { it.url }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
||||
val R = 6371000.0 // meters
|
||||
val dLat = Math.toRadians(lat2 - lat1)
|
||||
val dLon = Math.toRadians(lon2 - lon1)
|
||||
val a = sin(dLat / 2).pow(2.0) + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * sin(dLon / 2).pow(2.0)
|
||||
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
return R * c
|
||||
}
|
||||
|
||||
private fun normalizeRelayUrl(raw: String): String {
|
||||
val trimmed = raw.trim()
|
||||
if (trimmed.isEmpty()) return trimmed
|
||||
return if ("://" in trimmed) trimmed else "wss://$trimmed"
|
||||
}
|
||||
|
||||
// ===== Implementation details =====
|
||||
|
||||
private fun getPrefs(application: Application): SharedPreferences =
|
||||
application.getSharedPreferences(PREFS_NAME, Application.MODE_PRIVATE)
|
||||
|
||||
private fun getDownloadedFile(application: Application): File =
|
||||
File(application.filesDir, DOWNLOADED_FILE)
|
||||
|
||||
private fun isStale(application: Application): Boolean {
|
||||
val last = getPrefs(application).getLong(KEY_LAST_UPDATE_MS, 0L)
|
||||
val now = System.currentTimeMillis()
|
||||
return now - last >= ONE_DAY_MS
|
||||
}
|
||||
|
||||
private fun startPeriodicRefresh(application: Application) {
|
||||
ioScope.launch {
|
||||
while (true) {
|
||||
try {
|
||||
if (isStale(application)) {
|
||||
fetchAndMaybeSwap(application)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Periodic refresh encountered an error: ${e.message}")
|
||||
}
|
||||
delay(TimeUnit.MINUTES.toMillis(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAndMaybeSwap(application: Application) {
|
||||
try {
|
||||
val tmpFile = File.createTempFile("relays_", ".csv", application.cacheDir)
|
||||
val ok = downloadToFile(ASSET_FILE_URL, tmpFile)
|
||||
if (!ok) {
|
||||
Log.w(TAG, "Failed to fetch latest relays; keeping current list (will fallback to bundled if none)")
|
||||
tmpFile.delete()
|
||||
return
|
||||
}
|
||||
|
||||
val parsed = parseCsv(FileInputStream(tmpFile))
|
||||
if (parsed.isEmpty()) {
|
||||
Log.w(TAG, "Downloaded relay CSV parsed to 0 entries; ignoring")
|
||||
tmpFile.delete()
|
||||
return
|
||||
}
|
||||
|
||||
val dest = getDownloadedFile(application)
|
||||
tmpFile.inputStream().use { input ->
|
||||
FileOutputStream(dest, false).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
tmpFile.delete()
|
||||
|
||||
val hash = fileSha256Hex(dest)
|
||||
val entries = parsed.size
|
||||
|
||||
synchronized(relaysLock) {
|
||||
relays.clear()
|
||||
relays.addAll(parsed)
|
||||
}
|
||||
|
||||
getPrefs(application).edit().putLong(KEY_LAST_UPDATE_MS, System.currentTimeMillis()).apply()
|
||||
|
||||
Log.i(TAG, "✅ Using downloaded relay list (${dest.absolutePath}), entries=$entries, sha256=$hash, updatedAtMs=${getPrefs(application).getLong(KEY_LAST_UPDATE_MS, 0L)}")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to fetch and swap relay list: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadToFile(url: String, dest: File): Boolean {
|
||||
return try {
|
||||
val req = Request.Builder().url(url).get().build()
|
||||
httpClient.newCall(req).execute().use { resp ->
|
||||
if (!resp.isSuccessful) {
|
||||
Log.w(TAG, "HTTP ${'$'}{resp.code} when fetching $url")
|
||||
return false
|
||||
}
|
||||
val body = resp.body ?: return false
|
||||
FileOutputStream(dest).use { out ->
|
||||
body.byteStream().use { input ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Download error: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromFile(file: File, sourceLabel: String): Boolean {
|
||||
return try {
|
||||
val list = parseCsv(FileInputStream(file))
|
||||
if (list.isEmpty()) {
|
||||
Log.w(TAG, "${sourceLabel} relay CSV has 0 entries; ignoring")
|
||||
false
|
||||
} else {
|
||||
synchronized(relaysLock) {
|
||||
relays.clear()
|
||||
relays.addAll(list)
|
||||
}
|
||||
val hash = fileSha256Hex(file)
|
||||
Log.i(TAG, "📄 Loaded ${list.size} relay entries from ${sourceLabel} file (${file.absolutePath}), sha256=$hash")
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed loading ${sourceLabel} relay file: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromAssets(application: Application) {
|
||||
val list = try {
|
||||
parseCsv(application.assets.open(ASSET_FILE))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to open asset $ASSET_FILE: ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
synchronized(relaysLock) {
|
||||
relays.clear()
|
||||
relays.addAll(list)
|
||||
}
|
||||
// Compute asset hash for logging
|
||||
val hash = try {
|
||||
application.assets.open(ASSET_FILE).use { input ->
|
||||
streamSha256Hex(input)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"error:${'$'}{e.message}"
|
||||
}
|
||||
Log.i(TAG, "📦 Loaded ${list.size} relay entries from assets/$ASSET_FILE, sha256=$hash")
|
||||
}
|
||||
|
||||
private fun parseCsv(input: InputStream): List<RelayInfo> {
|
||||
val result = mutableListOf<RelayInfo>()
|
||||
BufferedReader(InputStreamReader(input)).use { reader ->
|
||||
var line: String?
|
||||
while (true) {
|
||||
line = reader.readLine()
|
||||
if (line == null) break
|
||||
val trimmed = line!!.trim()
|
||||
if (trimmed.isEmpty()) continue
|
||||
if (trimmed.lowercase().startsWith("relay url")) continue
|
||||
val parts = trimmed.split(",")
|
||||
if (parts.size < 3) continue
|
||||
val url = normalizeRelayUrl(parts[0].trim())
|
||||
val lat = parts[1].trim().toDoubleOrNull()
|
||||
val lon = parts[2].trim().toDoubleOrNull()
|
||||
if (url.isEmpty() || lat == null || lon == null) continue
|
||||
result.add(RelayInfo(url = url, latitude = lat, longitude = lon))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun fileSha256Hex(file: File): String = try {
|
||||
FileInputStream(file).use { input ->
|
||||
streamSha256Hex(input)
|
||||
}
|
||||
} catch (_: Exception) { "error" }
|
||||
|
||||
private fun streamSha256Hex(input: InputStream): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val buf = ByteArray(8192)
|
||||
var read: Int
|
||||
while (true) {
|
||||
read = input.read(buf)
|
||||
if (read <= 0) break
|
||||
digest.update(buf, 0, read)
|
||||
}
|
||||
val bytes = digest.digest()
|
||||
return bytes.joinToString("") { b ->
|
||||
val v = b.toInt() and 0xff
|
||||
val s = Integer.toHexString(v)
|
||||
if (s.length == 1) "0$s" else s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user