mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 14:25:21 +00:00
Nostr geohash (#276)
* first nostr build * add test file * internet access * fix relay manager * fix serialization * demo service - remove later * fix nostr * event dedupe * dedupe * ui wip * can send messages * subscription works * works * favs * works * delete chat on change * fix mentions * remove autojoin channels * styling * adjust colors * ui changes * live updates working * use local timestamp * message history in background * robust * fixes * nicknames refresh optimization * nostr service * refactor nostr * style * geohash works * centralize colors * refactoring * disable DMs for now: click on peer nickname doesnt open chat list in geohash mode * use local time * less logging * robustness * scroll nickname * adjust some text
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.PrivateMessagePacket
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* BitChat-over-Nostr Adapter
|
||||
* Direct port from iOS implementation for 100% compatibility
|
||||
*/
|
||||
object NostrEmbeddedBitChat {
|
||||
|
||||
private const val TAG = "NostrEmbeddedBitChat"
|
||||
|
||||
/**
|
||||
* Build a `bitchat1:` base64url-encoded BitChat packet carrying a private message for Nostr DMs.
|
||||
*/
|
||||
fun encodePMForNostr(
|
||||
content: String,
|
||||
messageID: String,
|
||||
recipientPeerID: String,
|
||||
senderPeerID: String
|
||||
): String? {
|
||||
try {
|
||||
// TLV-encode the private message
|
||||
val pm = PrivateMessagePacket(messageID = messageID, content = content)
|
||||
val tlv = pm.encode() ?: return null
|
||||
|
||||
// Prefix with NoisePayloadType
|
||||
val payload = ByteArray(1 + tlv.size)
|
||||
payload[0] = NoisePayloadType.PRIVATE_MESSAGE.value.toByte()
|
||||
System.arraycopy(tlv, 0, payload, 1, tlv.size)
|
||||
|
||||
// Determine 8-byte recipient ID to embed
|
||||
val recipientIDHex = normalizeRecipientPeerID(recipientPeerID)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
recipientID = hexStringToByteArray(recipientIDHex),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
ttl = 7u
|
||||
)
|
||||
|
||||
val data = packet.toBinaryData() ?: return null
|
||||
return "bitchat1:" + base64URLEncode(data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encode PM for Nostr: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `bitchat1:` base64url-encoded BitChat packet carrying a delivery/read ack for Nostr DMs.
|
||||
*/
|
||||
fun encodeAckForNostr(
|
||||
type: NoisePayloadType,
|
||||
messageID: String,
|
||||
recipientPeerID: String,
|
||||
senderPeerID: String
|
||||
): String? {
|
||||
if (type != NoisePayloadType.DELIVERED && type != NoisePayloadType.READ_RECEIPT) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val payload = ByteArray(1 + messageID.toByteArray(Charsets.UTF_8).size)
|
||||
payload[0] = type.value.toByte()
|
||||
val messageIDBytes = messageID.toByteArray(Charsets.UTF_8)
|
||||
System.arraycopy(messageIDBytes, 0, payload, 1, messageIDBytes.size)
|
||||
|
||||
val recipientIDHex = normalizeRecipientPeerID(recipientPeerID)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
recipientID = hexStringToByteArray(recipientIDHex),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
ttl = 7u
|
||||
)
|
||||
|
||||
val data = packet.toBinaryData() ?: return null
|
||||
return "bitchat1:" + base64URLEncode(data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encode ACK for Nostr: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `bitchat1:` ACK (delivered/read) without an embedded recipient peer ID (geohash DMs).
|
||||
*/
|
||||
fun encodeAckForNostrNoRecipient(
|
||||
type: NoisePayloadType,
|
||||
messageID: String,
|
||||
senderPeerID: String
|
||||
): String? {
|
||||
if (type != NoisePayloadType.DELIVERED && type != NoisePayloadType.READ_RECEIPT) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val payload = ByteArray(1 + messageID.toByteArray(Charsets.UTF_8).size)
|
||||
payload[0] = type.value.toByte()
|
||||
val messageIDBytes = messageID.toByteArray(Charsets.UTF_8)
|
||||
System.arraycopy(messageIDBytes, 0, payload, 1, messageIDBytes.size)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
recipientID = null, // No recipient for geohash DMs
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
ttl = 7u
|
||||
)
|
||||
|
||||
val data = packet.toBinaryData() ?: return null
|
||||
return "bitchat1:" + base64URLEncode(data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encode ACK for Nostr (no recipient): ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `bitchat1:` payload without an embedded recipient peer ID (used for geohash DMs).
|
||||
*/
|
||||
fun encodePMForNostrNoRecipient(
|
||||
content: String,
|
||||
messageID: String,
|
||||
senderPeerID: String
|
||||
): String? {
|
||||
try {
|
||||
val pm = PrivateMessagePacket(messageID = messageID, content = content)
|
||||
val tlv = pm.encode() ?: return null
|
||||
|
||||
val payload = ByteArray(1 + tlv.size)
|
||||
payload[0] = NoisePayloadType.PRIVATE_MESSAGE.value.toByte()
|
||||
System.arraycopy(tlv, 0, payload, 1, tlv.size)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
recipientID = null, // No recipient for geohash DMs
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
ttl = 7u
|
||||
)
|
||||
|
||||
val data = packet.toBinaryData() ?: return null
|
||||
return "bitchat1:" + base64URLEncode(data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encode PM for Nostr (no recipient): ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize recipient peer ID (matches iOS implementation)
|
||||
*/
|
||||
private fun normalizeRecipientPeerID(recipientPeerID: String): String {
|
||||
try {
|
||||
val maybeData = hexStringToByteArray(recipientPeerID)
|
||||
return when (maybeData.size) {
|
||||
32 -> {
|
||||
// Treat as Noise static public key; derive peerID from fingerprint
|
||||
// For now, return first 8 bytes as hex (simplified)
|
||||
maybeData.take(8).joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
8 -> {
|
||||
// Already an 8-byte peer ID
|
||||
recipientPeerID
|
||||
}
|
||||
else -> {
|
||||
// Fallback: return as-is (expecting 16 hex chars)
|
||||
recipientPeerID
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Fallback: return as-is
|
||||
return recipientPeerID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64url encode without padding (matches iOS implementation)
|
||||
*/
|
||||
private fun base64URLEncode(data: ByteArray): String {
|
||||
val b64 = Base64.encodeToString(data, Base64.NO_WRAP)
|
||||
return b64
|
||||
.replace("+", "-")
|
||||
.replace("/", "_")
|
||||
.replace("=", "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string to byte array
|
||||
*/
|
||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
||||
if (hexString.length % 2 != 0) {
|
||||
return ByteArray(8) // Return 8-byte array filled with zeros
|
||||
}
|
||||
|
||||
val result = ByteArray(8) { 0 } // Exactly 8 bytes like iOS
|
||||
var tempID = hexString
|
||||
var index = 0
|
||||
|
||||
while (tempID.length >= 2 && index < 8) {
|
||||
val hexByte = tempID.substring(0, 2)
|
||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
||||
if (byte != null) {
|
||||
result[index] = byte
|
||||
}
|
||||
tempID = tempID.substring(2)
|
||||
index++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user