Implement Geohash Presence (Heartbeats) (#576)

* geohash announce

* only for some geohashes

* global presence right away

* jitter delays

* show ? people for high-precision geohashes

* 1000 events
This commit is contained in:
callebtc
2026-01-13 03:23:53 +07:00
committed by GitHub
parent 654d385b6d
commit d164b3f9bc
7 changed files with 229 additions and 13 deletions
@@ -46,15 +46,17 @@ class GeohashMessageHandler(
fun onEvent(event: NostrEvent, subscribedGeohash: String) {
scope.launch(Dispatchers.Default) {
try {
if (event.kind != 20000) return@launch
if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch
val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1)
if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch
if (dedupe(event.id)) return@launch
// PoW validation (if enabled)
val pow = PoWPreferenceManager.getCurrentSettings()
if (pow.enabled && pow.difficulty > 0) {
if (!NostrProofOfWork.validateDifficulty(event, pow.difficulty)) return@launch
// PoW validation (if enabled) - apply to chat messages primarily
if (event.kind == NostrKind.EPHEMERAL_EVENT) {
val pow = PoWPreferenceManager.getCurrentSettings()
if (pow.enabled && pow.difficulty > 0) {
if (!NostrProofOfWork.validateDifficulty(event, pow.difficulty)) return@launch
}
}
// Blocked users check (use injected DataManager which has loaded state)
@@ -62,7 +64,12 @@ class GeohashMessageHandler(
// Update repository (participants, nickname, teleport)
// Update repository on a background-safe path; repository will post updates to LiveData
repo.updateParticipant(subscribedGeohash, event.pubkey, Date(event.createdAt * 1000L))
// Update participant count (last seen) on BOTH Presence (20001) and Chat (20000) events
if (event.kind == NostrKind.GEOHASH_PRESENCE || event.kind == NostrKind.EPHEMERAL_EVENT) {
repo.updateParticipant(subscribedGeohash, event.pubkey, Date(event.createdAt * 1000L))
}
event.tags.find { it.size >= 2 && it[0] == "n" }?.let { repo.cacheNickname(event.pubkey, it[1]) }
event.tags.find { it.size >= 2 && it[0] == "t" && it[1] == "teleport" }?.let { repo.markTeleported(event.pubkey) }
// Register a geohash DM alias for this participant so MessageRouter can route DMs via Nostr
@@ -70,6 +77,9 @@ class GeohashMessageHandler(
com.bitchat.android.nostr.GeohashAliasRegistry.put("nostr_${event.pubkey.take(16)}", event.pubkey)
} catch (_: Exception) { }
// Stop here for presence events - they don't produce chat messages
if (event.kind == NostrKind.GEOHASH_PRESENCE) return@launch
// Skip our own events for message emission
val my = NostrIdentityBridge.deriveIdentity(subscribedGeohash, application)
if (my.publicKeyHex.equals(event.pubkey, true)) return@launch
@@ -215,6 +215,7 @@ object NostrKind {
const val SEAL = 13 // NIP-17 sealed event
const val GIFT_WRAP = 1059 // NIP-17 gift wrap
const val EPHEMERAL_EVENT = 20000 // For geohash channels
const val GEOHASH_PRESENCE = 20001 // For geohash presence heartbeat
}
/**
@@ -32,11 +32,11 @@ data class NostrFilter(
}
/**
* Create filter for geohash-scoped ephemeral events (kind 20000)
* Create filter for geohash-scoped ephemeral events (kind 20000 and 20001)
*/
fun geohashEphemeral(geohash: String, since: Long? = null, limit: Int = 200): NostrFilter {
fun geohashEphemeral(geohash: String, since: Long? = null, limit: Int = 1000): NostrFilter {
return NostrFilter(
kinds = listOf(NostrKind.EPHEMERAL_EVENT),
kinds = listOf(NostrKind.EPHEMERAL_EVENT, NostrKind.GEOHASH_PRESENCE),
since = since?.let { (it / 1000).toInt() },
tagFilters = mapOf("g" to listOf(geohash)),
limit = limit
@@ -117,6 +117,28 @@ object NostrProtocol {
return@withContext senderIdentity.signEvent(event)
}
/**
* Create a geohash-scoped presence event (kind 20001)
* Has no content and no nickname, used for participant counting
*/
suspend fun createGeohashPresenceEvent(
geohash: String,
senderIdentity: NostrIdentity
): NostrEvent = withContext(Dispatchers.Default) {
val tags = mutableListOf<List<String>>()
tags.add(listOf("g", geohash))
val event = NostrEvent(
pubkey = senderIdentity.publicKeyHex,
createdAt = (System.currentTimeMillis() / 1000).toInt(),
kind = NostrKind.GEOHASH_PRESENCE,
tags = tags,
content = ""
)
return@withContext senderIdentity.signEvent(event)
}
/**
* Create a geohash-scoped ephemeral public message (kind 20000)