mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 07:25:19 +00:00
New MessageType 0x23 carries TLV-encoded board posts and tombstones,
self-signed with the author's Ed25519 key ("bitchat-board-v1" /
"bitchat-board-del-v1" domains) so notices verify without the author
present. BoardStore persists raw signed packets under Application
Support/board/ (200 posts, 5 per author, oldest evicted; expiry sweep;
tombstones retained until the deleted post's original expiry) and is
wiped on panic.
Board packets join gossip sync as bit 8 of the existing variable-length
types bitfield (a second byte old decoders already accept and ignore),
with a 60s round and its own capacity, served straight from the board
store so retention has one owner. Posts relay like broadcasts; urgent
posts get the announce-class TTL cap.
UI: a pin button in the header opens the board for the current channel
(geohash board, or mesh-local board), with urgent-pinned newest-first
listing, compose with urgent toggle and 1/3/7-day expiry, and
swipe-delete on own posts. Geohash posts also publish one-way as
Nostr kind-1 location notes when relays are reachable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
3.8 KiB
Swift
115 lines
3.8 KiB
Swift
import BitFoundation
|
|
import Foundation
|
|
|
|
/// Bitfield describing which message types are covered by a REQUEST_SYNC round.
|
|
/// Matches the Android mapping (bit index -> message type).
|
|
struct SyncTypeFlags: OptionSet {
|
|
let rawValue: UInt64
|
|
|
|
init(rawValue: UInt64) {
|
|
self.rawValue = rawValue & 0x00FF_FFFF_FFFF_FFFF // Trim to max 8 bytes
|
|
}
|
|
|
|
private static func bitIndex(for type: MessageType) -> Int? {
|
|
switch type {
|
|
case .announce: return 0
|
|
case .message: return 1
|
|
case .leave: return 2
|
|
case .noiseHandshake: return 3
|
|
case .noiseEncrypted: return 4
|
|
case .fragment: return 5
|
|
case .requestSync: return 6
|
|
case .fileTransfer: return 7
|
|
case .boardPost: return 8
|
|
// Courier envelopes are directed deposits between trusted peers and
|
|
// must never spread via gossip sync.
|
|
case .courierEnvelope: return nil
|
|
}
|
|
}
|
|
|
|
private static func type(forBit index: Int) -> MessageType? {
|
|
switch index {
|
|
case 0: return .announce
|
|
case 1: return .message
|
|
case 2: return .leave
|
|
case 3: return .noiseHandshake
|
|
case 4: return .noiseEncrypted
|
|
case 5: return .fragment
|
|
case 6: return .requestSync
|
|
case 7: return .fileTransfer
|
|
// Bit 8 spills the encoded bitfield into a second byte. Decoders since
|
|
// type-aware sync (#853) accept 1-8 bytes and map unknown bits to no
|
|
// known type, so old clients ignore board rounds instead of choking.
|
|
case 8: return .boardPost
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static let announce = SyncTypeFlags(messageTypes: [.announce])
|
|
static let message = SyncTypeFlags(messageTypes: [.message])
|
|
static let fragment = SyncTypeFlags(messageTypes: [.fragment])
|
|
static let fileTransfer = SyncTypeFlags(messageTypes: [.fileTransfer])
|
|
static let board = SyncTypeFlags(messageTypes: [.boardPost])
|
|
|
|
static let publicMessages = SyncTypeFlags(messageTypes: [.announce, .message])
|
|
|
|
init(messageTypes: [MessageType]) {
|
|
var raw: UInt64 = 0
|
|
for type in messageTypes {
|
|
guard let bit = SyncTypeFlags.bitIndex(for: type) else { continue }
|
|
raw |= (1 << UInt64(bit))
|
|
}
|
|
self.init(rawValue: raw)
|
|
}
|
|
|
|
func contains(_ type: MessageType) -> Bool {
|
|
guard let bit = SyncTypeFlags.bitIndex(for: type) else { return false }
|
|
return contains(SyncTypeFlags(rawValue: 1 << UInt64(bit)))
|
|
}
|
|
|
|
func union(_ other: SyncTypeFlags) -> SyncTypeFlags {
|
|
SyncTypeFlags(rawValue: rawValue | other.rawValue)
|
|
}
|
|
|
|
func intersection(_ other: SyncTypeFlags) -> SyncTypeFlags {
|
|
SyncTypeFlags(rawValue: rawValue & other.rawValue)
|
|
}
|
|
|
|
func toMessageTypes() -> [MessageType] {
|
|
guard rawValue != 0 else { return [] }
|
|
var types: [MessageType] = []
|
|
for bit in 0..<64 {
|
|
guard (rawValue & (1 << UInt64(bit))) != 0 else { continue }
|
|
if let type = SyncTypeFlags.type(forBit: bit) {
|
|
types.append(type)
|
|
}
|
|
}
|
|
return types
|
|
}
|
|
|
|
func toData() -> Data? {
|
|
guard rawValue != 0 else { return nil }
|
|
var value = rawValue
|
|
var bytes: [UInt8] = []
|
|
while value > 0 && bytes.count < 8 {
|
|
bytes.append(UInt8(value & 0xFF))
|
|
value >>= 8
|
|
}
|
|
while let last = bytes.last, last == 0 {
|
|
bytes.removeLast()
|
|
}
|
|
guard !bytes.isEmpty, bytes.count <= 8 else { return nil }
|
|
return Data(bytes)
|
|
}
|
|
|
|
static func decode(_ data: Data) -> SyncTypeFlags? {
|
|
guard (1...8).contains(data.count) else { return nil }
|
|
var raw: UInt64 = 0
|
|
for (index, byte) in data.enumerated() {
|
|
raw |= UInt64(byte) << UInt64(index * 8)
|
|
}
|
|
return SyncTypeFlags(rawValue: raw)
|
|
}
|
|
}
|