Merge 8cc56f8ddd718b79f42c2c5bc0374f215d83b0e7 into 1f59e814f90c3f489f48d68262cb1bf640bf6181

This commit is contained in:
Taksh Kothari 2026-08-06 01:44:30 +00:00 committed by GitHub
commit ee15f5c7d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 0 deletions

View File

@ -81,6 +81,7 @@ construction, not NIP-44 encryption.
- `region` (2 chars): Country/large region
- **Internet**: Required (connects to Nostr relays)
- **Use Case**: Location-based community chat, local events, regional discussions
- **External publishers:** relays are chosen by proximity to the geohash — see [Geohash publisher relays](docs/GEOHASH-PUBLISHER-RELAYS.md)
### Direct Message Routing

View File

@ -0,0 +1,116 @@
# Geohash Publisher Relay Selection & Delivery
Guide for external tools that publish into BitChat location channels (kind
`20000` chat, kind `20001` presence). BitChat clients do **not** use a fixed
global relay set for these channels — they pick relays by proximity to the
geohash cell. A publisher that targets the wrong relays will never reach those
clients.
## Event shape
Location chat messages are ephemeral Nostr events:
| Field | Value |
|-------|--------|
| `kind` | `20000` (chat) or `20001` (presence heartbeat) |
| `tags` | Must include `["g", "<geohash>"]`. Chat may also carry `["n", "<nickname>"]`. |
| `content` | Message text for kind `20000`; empty for kind `20001` |
| `pubkey` | Ephemeral identity derived for that geohash (not the person's long-term key) |
Clients subscribe with filters equivalent to:
```json
{ "kinds": [20000, 20001], "#g": ["<geohash>"], "limit": 200 }
```
See [GeohashPresenceSpec.md](GeohashPresenceSpec.md) for presence broadcast
rules (precision limits, heartbeat cadence).
## How clients choose relays
On subscribe and publish, BitChat asks `GeoRelayDirectory` for the closest
relays to the geohash center:
1. Decode the geohash to a lat/lon center.
2. Rank the geo-relay directory by haversine distance.
3. Take the nearest **5** relays (`TransportConfig.nostrGeoRelayCount`).
4. Ties break by hostname so every device with the same directory picks the
same set — publishers and subscribers must agree.
### Directory source of truth
Do **not** rely only on the CSV bundled inside an older app build. At runtime
the client refreshes and caches the reviewed copy on `main`:
https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv
That URL is `GeoRelayDirectory`'s `remoteURL`. External publishers should rank
against this same file (or a freshly synced cache of it). A snapshot shipped
with an old binary can diverge after relay GPS rows change, so messages land
on hosts current clients no longer subscribe to.
### Empty-directory fallback
`closestRelays` returns `[]` only when the in-memory directory has no entries
(or `count <= 0`). In that case the publish path does **not** refuse — it
falls back to `NostrRelayManager`'s default relay list (built-in + any custom
relays). See `ChatPublicConversationCoordinator.sendPublicRaw`: empty
`targetRelays``sendEvent(event)` with no explicit `to:`.
Publishers should still prefer proximity selection against the live CSV.
Default-relay fallback is a last resort when the geo directory failed to load;
relying on it intentionally will miss clients that *did* load the directory
and are subscribed only to up to the nearest five (fewer when the directory
is thin; none when it is empty — see the fallback above).
## Sparse cells (stale directory rows)
A "sparse" cell is about **directory freshness**, not geographic remoteness.
Haversine ranking returns *up to* the nearest five hosts from whatever GPS
rows the CSV currently has — including for oceans or deserts — and fewer
than five when the directory is thin. When those rows are stale or thin for
a region (relay moved, GPS never updated, host offline), the selected hosts
can be the wrong place for live traffic even though they look "close" on
paper.
The channel UI still opens normally — there is no separate "undeliverable"
banner — but live messages may not round-trip. Treat an empty timeline in those
cells as a stale/coverage problem in the relay directory, not a client bug.
Refreshing from the canonical CSV above is the first fix to try.
## Ephemerality and late joiners
Kind `20000` / `20001` events are ephemeral. Relays typically do not store them
for historical replay. A client that subscribes minutes later cannot retrieve
messages that already left the relay's ephemeral buffer. Initial lookback on
subscribe is on the order of one hour for chat sampling
(`nostrGeohashInitialLookbackSeconds`), but that only recovers what the chosen
relays still hold.
For durable geographic notes, BitChat uses persistent kind `1` events tagged
with `g` (location notes / drops) — a different surface from live channel chat.
## Practical checklist for external publishers
1. Compute the target geohash at the precision the channel uses (`block` ≈ 7,
`neighborhood` ≈ 6, `city` ≈ 5, etc.).
2. Fetch the **current**
[`online_relays_gps.csv`](https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv)
(same URL the app refreshes from). Rank by haversine to the geohash center
and take the nearest ~5 `wss://` hosts (hostname tie-break). Optionally
publish to a strict **superset** that covers both that selection and any
older bundled snapshot you still support.
3. Publish the signed kind-`20000` event to those relays with a correct `g` tag.
4. Expect no backlog for late subscribers; design tools around live delivery.
5. Do not assume App Store / mesh Bluetooth peers will see Nostr-only publishes
— mesh and geohash are separate transports.
## Related code
- `bitchat/Nostr/GeoRelayDirectory.swift` — proximity ranking, `remoteURL`, cache
- `bitchat/ViewModels/ChatPublicConversationCoordinator.swift` — empty-selection
fallback to default relays
- `bitchat/Services/GeohashPresenceService.swift` — subscribe/publish wiring
- `bitchat/Services/TransportConfig.swift``nostrGeoRelayCount`, lookback limits
- `bitchat/Nostr/NostrProtocol.swift` — kind `20000` / `20001` builders
- `relays/online_relays_gps.csv` — reviewed directory checked into this repo

View File

@ -94,3 +94,9 @@ The presentation of the participant count depends on the geohash precision level
* **Privacy:** High-precision location presence is NOT broadcast. Temporal correlation between different levels is obfuscated via random delays.
* **Consistency:** "Online" status is maintained globally while the app is open.
* **Transparency:** The UI correctly reflects uncertainty (`?`) when privacy rules prevent accurate passive counting.
## External publishers
Tools that publish kind `20000` / `20001` into a geohash channel must target the
same proximity-selected relays BitChat clients use, or messages will not arrive.
See [GEOHASH-PUBLISHER-RELAYS.md](GEOHASH-PUBLISHER-RELAYS.md).