mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 15:25:22 +00:00
* Add capability bits to announce TLV Announces now carry an optional capabilities TLV (0x05): a little-endian bitfield with named bits for upcoming features (prekeys, wifiBulk, gateway, groups, board, vouch, meshDiagnostics). Old clients skip the unknown TLV; peers without it decode as nil so features can distinguish "legacy peer" from "advertises nothing". PeerCapabilities lives in BitFoundation with a minimal-length encoding that preserves unknown bits for forward compatibility. Peer capabilities are stored in the BLE peer registry on verified announce and exposed via BLEService.peerCapabilities(_:). The local advertisement set is empty until each feature ships its bit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Gateway mode: opt-in mesh↔Nostr uplink for geohash channels An opt-in "internet gateway" toggle lets one connected phone bridge the local geohash channel for mesh-only peers: signed kind-20000 events ride a new nostrCarrier (0x28) packet — directed to the gateway for uplink, broadcast with TTL for downlink — with Schnorr verification at every hop, CourierStore-style quotas, and explicit loop-prevention rules. - BitFoundation: MessageType.nostrCarrier = 0x28 - NostrCarrierPacket: 2-byte-length TLV codec (direction, geohash, signed event JSON), 16 KiB cap, tolerant decoder - GatewayService: closure-injected policy layer — verify gates (sig, kind, #g tag, age, size), uplink quotas (10/min/depositor rate limit, offline queue of 20 total / 5 per depositor, drop-oldest, flush on reconnect), downlink budget (30/min, bounded drop-oldest backlog), bounded loop-prevention ID sets - BLEService: runtime capability bits (advertise .gateway only while the toggle is on, re-announce on change), signed directed uplink sends, carrier ingress with depositor signature verification - Mesh-only senders uplink automatically from sendGeohash when no relay is connected and a reachable peer advertises .gateway; once-per- channel "sent via mesh gateway" notice - UI: gateway toggle beside the Tor toggle, globe header indicator, VoiceOver labels, xcstrings entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Gateway: harden downlink freshness, uplink verify ordering, and drain Fixes the confirmed downlink/uplink defects from the PR #1384 review + Codex findings: - Downlink age + #g gate (Codex P2 / review #1): rebroadcastRelayEvent now drops events outside the same freshness window receivers enforce and whose #g tag mismatches the carrier geohash, BEFORE spending any budget — so a 1h/200-event channel-resubscribe backfill no longer burns the 30/min BLE budget on events every receiver drops. - Rate-limit + dedup before Schnorr (review #2): handleUplinkDeposit now runs cheap structural checks + carried-ID dedup + rate-token consume before isValidSignature(), so a replay flood is bounded by cheap work instead of unbounded main-actor verifies. - Quota-dropped deposits not rendered (review #3): enqueueUplink reports acceptance and injectInbound only fires for events actually published/queued, ending the local-timeline divergence. - Drain timer + mark-after-send (Codex P2 / review #4): a burst beyond budget now arms a timer to drain when the window frees; rebroadcast IDs are marked only after an event is actually sent, so overflow- dropped events stay retryable. - Symmetric publish path (review #5): the gateway publish closure now refuses when no geo relay is known, matching the local send path instead of publishing dead traffic to default relays. - Loop-rule doc (review #7): softened to reflect that rule 3 is a call-site convention with unit-tested backstops; added tests for the publishedEventIDs backstop, downlink freshness/mismatch, drain timer, and quota-drop non-injection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Gateway: stop self-echo of uplinked events onto the mesh Every event a gateway uplinks to the relays comes back through its own geohash subscription. `rebroadcastRelayEvent` deduped against `meshBroadcastEventIDs`, `rebroadcastEventIDs`, and `pendingDownlinks`, but not `publishedEventIDs` — so an event this gateway just published was downlink-rebroadcast onto the same mesh it originated from, doubling BLE airtime per uplinked message and able to starve the 30/min downlink budget on a busy channel (device-confirmed, filed on #1384). Fix: also skip the downlink rebroadcast when the event id is in `publishedEventIDs`. That set is already the bounded (drop-oldest, capacity maxTrackedEventIDs) loop-rule-2 uplink cache, populated only by `publish()`, so genuine inbound-from-internet events (never published here) still rebroadcast normally. Reconciles cleanly with the existing loop-prevention sets — no new state. Adds a GatewayServiceTests case asserting an uplinked event that echoes back via the subscription is not rebroadcast, while a genuine inbound event still is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
191 lines
5.8 KiB
Swift
191 lines
5.8 KiB
Swift
import BitFoundation
|
|
import Combine
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class LocationChannelsModel: ObservableObject {
|
|
@Published private(set) var permissionState: LocationChannelManager.PermissionState
|
|
@Published private(set) var availableChannels: [GeohashChannel]
|
|
@Published private(set) var selectedChannel: ChannelID
|
|
@Published private(set) var teleported: Bool
|
|
@Published private(set) var bookmarks: [String]
|
|
@Published private(set) var bookmarkNames: [String: String]
|
|
@Published private(set) var locationNames: [GeohashChannelLevel: String]
|
|
@Published private(set) var userTorEnabled: Bool
|
|
@Published private(set) var gatewayEnabled: Bool
|
|
|
|
private let manager: LocationChannelManager
|
|
private let network: NetworkActivationService
|
|
private let gateway: GatewayService
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
init(
|
|
manager: LocationChannelManager? = nil,
|
|
network: NetworkActivationService? = nil,
|
|
gateway: GatewayService? = nil
|
|
) {
|
|
let manager = manager ?? .shared
|
|
let network = network ?? .shared
|
|
let gateway = gateway ?? .shared
|
|
|
|
self.manager = manager
|
|
self.network = network
|
|
self.gateway = gateway
|
|
self.gatewayEnabled = gateway.isEnabled
|
|
self.permissionState = manager.permissionState
|
|
self.availableChannels = manager.availableChannels
|
|
self.selectedChannel = manager.selectedChannel
|
|
self.teleported = manager.teleported
|
|
self.bookmarks = manager.bookmarks
|
|
self.bookmarkNames = manager.bookmarkNames
|
|
self.locationNames = manager.locationNames
|
|
self.userTorEnabled = network.userTorEnabled
|
|
|
|
bind()
|
|
}
|
|
|
|
var currentBuildingGeohash: String? {
|
|
availableChannels.first(where: { $0.level == .building })?.geohash
|
|
}
|
|
|
|
func isSelected(_ channel: GeohashChannel) -> Bool {
|
|
guard case .location(let selected) = selectedChannel else { return false }
|
|
return selected == channel
|
|
}
|
|
|
|
func isBookmarked(_ geohash: String) -> Bool {
|
|
manager.isBookmarked(geohash)
|
|
}
|
|
|
|
func enableLocationChannels() {
|
|
manager.enableLocationChannels()
|
|
}
|
|
|
|
func refreshChannels() {
|
|
manager.refreshChannels()
|
|
}
|
|
|
|
func enableAndRefresh() {
|
|
manager.enableLocationChannels()
|
|
manager.refreshChannels()
|
|
}
|
|
|
|
func beginLiveRefresh() {
|
|
manager.beginLiveRefresh()
|
|
}
|
|
|
|
func endLiveRefresh() {
|
|
manager.endLiveRefresh()
|
|
}
|
|
|
|
func select(_ channel: ChannelID) {
|
|
manager.select(channel)
|
|
}
|
|
|
|
func markTeleported(for geohash: String, _ flag: Bool) {
|
|
manager.markTeleported(for: geohash, flag)
|
|
}
|
|
|
|
func toggleBookmark(_ geohash: String) {
|
|
manager.toggleBookmark(geohash)
|
|
}
|
|
|
|
func resolveBookmarkNameIfNeeded(for geohash: String) {
|
|
manager.resolveBookmarkNameIfNeeded(for: geohash)
|
|
}
|
|
|
|
func locationName(for level: GeohashChannelLevel) -> String? {
|
|
locationNames[level]
|
|
}
|
|
|
|
func setUserTorEnabled(_ enabled: Bool) {
|
|
network.setUserTorEnabled(enabled)
|
|
}
|
|
|
|
func setGatewayEnabled(_ enabled: Bool) {
|
|
gateway.setEnabled(enabled)
|
|
}
|
|
|
|
func refreshMeshChannelsIfNeeded() {
|
|
guard case .mesh = selectedChannel,
|
|
permissionState == .authorized,
|
|
availableChannels.isEmpty else {
|
|
return
|
|
}
|
|
refreshChannels()
|
|
}
|
|
|
|
func openLocationChannel(for geohash: String) {
|
|
let normalized = geohash.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz")
|
|
guard (2...12).contains(normalized.count),
|
|
normalized.allSatisfy({ allowed.contains($0) }) else {
|
|
return
|
|
}
|
|
|
|
let channel = GeohashChannel(level: level(forLength: normalized.count), geohash: normalized)
|
|
let isRegional = availableChannels.contains { $0.geohash == normalized }
|
|
if !isRegional && !availableChannels.isEmpty {
|
|
markTeleported(for: normalized, true)
|
|
}
|
|
select(.location(channel))
|
|
}
|
|
|
|
func teleport(to geohash: String) {
|
|
let normalized = geohash.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
let channel = GeohashChannel(level: level(forLength: normalized.count), geohash: normalized)
|
|
markTeleported(for: normalized, true)
|
|
select(.location(channel))
|
|
}
|
|
|
|
private func bind() {
|
|
manager.$permissionState
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$permissionState)
|
|
|
|
manager.$availableChannels
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$availableChannels)
|
|
|
|
manager.$selectedChannel
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$selectedChannel)
|
|
|
|
manager.$teleported
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$teleported)
|
|
|
|
manager.$bookmarks
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$bookmarks)
|
|
|
|
manager.$bookmarkNames
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$bookmarkNames)
|
|
|
|
manager.$locationNames
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$locationNames)
|
|
|
|
network.$userTorEnabled
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$userTorEnabled)
|
|
|
|
gateway.$isEnabled
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: &$gatewayEnabled)
|
|
}
|
|
|
|
private func level(forLength length: Int) -> GeohashChannelLevel {
|
|
switch length {
|
|
case 0...2: return .region
|
|
case 3...4: return .province
|
|
case 5: return .city
|
|
case 6: return .neighborhood
|
|
case 7: return .block
|
|
case 8...12: return .building
|
|
default: return .block
|
|
}
|
|
}
|
|
}
|