mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-26 17:25:24 +00:00
refactor
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
@@ -7,16 +11,13 @@ import java.nio.ByteOrder
|
||||
* BitchatFilePacket: TLV-encoded file transfer payload for BLE mesh.
|
||||
* TLVs:
|
||||
* - 0x01: filename (UTF-8)
|
||||
* - 0x02: file size (8 bytes, UInt64)
|
||||
* - 0x02: file size (4 bytes, UInt32)
|
||||
* - 0x03: mime type (UTF-8)
|
||||
* - 0x04: content (bytes) — may appear multiple times for large files
|
||||
* - 0x04: content (bytes)
|
||||
*
|
||||
* Length field for TLV is 2 bytes (UInt16, big-endian) for all TLVs.
|
||||
* For large files, CONTENT is chunked into multiple TLVs of up to 65535 bytes each.
|
||||
*
|
||||
* Note: The outer BitchatPacket uses version 2 (4-byte payload length), so this
|
||||
* TLV payload can exceed 64 KiB even though each TLV value is limited to 65535 bytes.
|
||||
* Transport-level fragmentation then splits the final packet for BLE MTU.
|
||||
* FILE_NAME, FILE_SIZE, and MIME_TYPE use 2-byte big-endian lengths.
|
||||
* CONTENT uses a 4-byte big-endian length so the outer v2 packet can carry
|
||||
* payloads larger than 64 KiB before transport fragmentation.
|
||||
*/
|
||||
data class BitchatFilePacket(
|
||||
val fileName: String,
|
||||
@@ -30,87 +31,53 @@ data class BitchatFilePacket(
|
||||
}
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
try {
|
||||
android.util.Log.d("BitchatFilePacket", "🔄 Encoding: name=$fileName, size=$fileSize, mime=$mimeType")
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
val mimeBytes = mimeType.toByteArray(Charsets.UTF_8)
|
||||
// Validate bounds for 2-byte TLV lengths (per-TLV). CONTENT may exceed 65535 and will be chunked.
|
||||
if (nameBytes.size > 0xFFFF || mimeBytes.size > 0xFFFF) {
|
||||
android.util.Log.e("BitchatFilePacket", "❌ TLV field too large: name=${nameBytes.size}, mime=${mimeBytes.size} (max: 65535)")
|
||||
return try {
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
val mimeBytes = mimeType.toByteArray(Charsets.UTF_8)
|
||||
if (nameBytes.size > 0xFFFF || mimeBytes.size > 0xFFFF) {
|
||||
return null
|
||||
}
|
||||
if (content.size > 0xFFFF) {
|
||||
android.util.Log.d("BitchatFilePacket", "📦 Content exceeds 65535 bytes (${content.size}); will be split into multiple CONTENT TLVs")
|
||||
} else {
|
||||
android.util.Log.d("BitchatFilePacket", "📏 TLV sizes OK: name=${nameBytes.size}, mime=${mimeBytes.size}, content=${content.size}")
|
||||
}
|
||||
val sizeFieldLen = 4 // UInt32 for FILE_SIZE (changed from 8 bytes)
|
||||
val contentLenFieldLen = 4 // UInt32 for CONTENT TLV as requested
|
||||
|
||||
// Compute capacity: header TLVs + single CONTENT TLV with 4-byte length
|
||||
val contentTLVBytes = 1 + contentLenFieldLen + content.size
|
||||
val capacity = (1 + 2 + nameBytes.size) + (1 + 2 + sizeFieldLen) + (1 + 2 + mimeBytes.size) + contentTLVBytes
|
||||
val buf = ByteBuffer.allocate(capacity).order(ByteOrder.BIG_ENDIAN)
|
||||
val sizeBytes = ByteBuffer.allocate(4)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putInt(fileSize.toInt())
|
||||
.array()
|
||||
|
||||
// FILE_NAME
|
||||
buf.put(TLVType.FILE_NAME.v.toByte())
|
||||
buf.putShort(nameBytes.size.toShort())
|
||||
buf.put(nameBytes)
|
||||
|
||||
// FILE_SIZE (4 bytes)
|
||||
buf.put(TLVType.FILE_SIZE.v.toByte())
|
||||
buf.putShort(sizeFieldLen.toShort())
|
||||
buf.putInt(fileSize.toInt())
|
||||
|
||||
// MIME_TYPE
|
||||
buf.put(TLVType.MIME_TYPE.v.toByte())
|
||||
buf.putShort(mimeBytes.size.toShort())
|
||||
buf.put(mimeBytes)
|
||||
|
||||
// CONTENT (single TLV with 4-byte length)
|
||||
buf.put(TLVType.CONTENT.v.toByte())
|
||||
buf.putInt(content.size)
|
||||
buf.put(content)
|
||||
|
||||
val result = buf.array()
|
||||
android.util.Log.d("BitchatFilePacket", "✅ Encoded successfully: ${result.size} bytes total")
|
||||
return result
|
||||
TlvWriter()
|
||||
.put(TLVType.FILE_NAME.v.toInt(), nameBytes, TlvLengthSize.TWO_BYTES)
|
||||
.put(TLVType.FILE_SIZE.v.toInt(), sizeBytes, TlvLengthSize.TWO_BYTES)
|
||||
.put(TLVType.MIME_TYPE.v.toInt(), mimeBytes, TlvLengthSize.TWO_BYTES)
|
||||
.put(TLVType.CONTENT.v.toInt(), content, TlvLengthSize.FOUR_BYTES)
|
||||
.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("BitchatFilePacket", "❌ Encoding failed: ${e.message}", e)
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(data: ByteArray): BitchatFilePacket? {
|
||||
android.util.Log.d("BitchatFilePacket", "🔄 Decoding ${data.size} bytes")
|
||||
try {
|
||||
var off = 0
|
||||
val knownTypes = TLVType.values().map { it.v.toInt() }.toSet()
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.TWO_BYTES,
|
||||
unknownPolicy = UnknownTlvPolicy.FAIL,
|
||||
knownTypes = knownTypes
|
||||
) { type ->
|
||||
if (type == TLVType.CONTENT.v.toInt()) TlvLengthSize.FOUR_BYTES else TlvLengthSize.TWO_BYTES
|
||||
} ?: return null
|
||||
|
||||
var name: String? = null
|
||||
var size: Long? = null
|
||||
var mime: String? = null
|
||||
var contentBytes: ByteArray? = null
|
||||
while (off + 3 <= data.size) { // minimum TLV header size (type + 2 bytes length)
|
||||
val t = TLVType.from(data[off].toUByte()) ?: return null
|
||||
off += 1
|
||||
// CONTENT uses 4-byte length; others use 2-byte length
|
||||
val len: Int
|
||||
if (t == TLVType.CONTENT) {
|
||||
if (off + 4 > data.size) return null
|
||||
len = ((data[off].toInt() and 0xFF) shl 24) or ((data[off + 1].toInt() and 0xFF) shl 16) or ((data[off + 2].toInt() and 0xFF) shl 8) or (data[off + 3].toInt() and 0xFF)
|
||||
off += 4
|
||||
} else {
|
||||
if (off + 2 > data.size) return null
|
||||
len = ((data[off].toInt() and 0xFF) shl 8) or (data[off + 1].toInt() and 0xFF)
|
||||
off += 2
|
||||
}
|
||||
if (len < 0 || off + len > data.size) return null
|
||||
val value = data.copyOfRange(off, off + len)
|
||||
off += len
|
||||
for (field in fields) {
|
||||
val t = TLVType.from(field.type.toUByte()) ?: return null
|
||||
val value = field.value
|
||||
when (t) {
|
||||
TLVType.FILE_NAME -> name = String(value, Charsets.UTF_8)
|
||||
TLVType.FILE_SIZE -> {
|
||||
if (len != 4) return null
|
||||
if (value.size != 4) return null
|
||||
val bb = ByteBuffer.wrap(value).order(ByteOrder.BIG_ENDIAN)
|
||||
size = bb.int.toLong()
|
||||
}
|
||||
@@ -128,14 +95,10 @@ data class BitchatFilePacket(
|
||||
val c = contentBytes ?: return null
|
||||
val s = size ?: c.size.toLong()
|
||||
val m = mime ?: "application/octet-stream"
|
||||
val result = BitchatFilePacket(n, s, m, c)
|
||||
android.util.Log.d("BitchatFilePacket", "✅ Decoded: name=$n, size=$s, mime=$m, content=${c.size} bytes")
|
||||
return result
|
||||
return BitchatFilePacket(n, s, m, c)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("BitchatFilePacket", "❌ Decoding failed: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
/**
|
||||
* FragmentPayload - 100% iOS-compatible fragment payload structure
|
||||
@@ -111,7 +112,7 @@ data class FragmentPayload(
|
||||
* Get fragment ID as hex string for logging/debugging
|
||||
*/
|
||||
fun getFragmentIDString(): String {
|
||||
return fragmentID.joinToString("") { "%02x".format(it) }
|
||||
return fragmentID.toHexString()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.bitchat.android.util.*
|
||||
|
||||
/**
|
||||
* Identity announcement structure with TLV encoding
|
||||
@@ -36,29 +40,15 @@ data class IdentityAnnouncement(
|
||||
fun encode(): ByteArray? {
|
||||
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Check size limits
|
||||
if (nicknameData.size > 255 || noisePublicKey.size > 255 || signingPublicKey.size > 255) {
|
||||
return null
|
||||
return try {
|
||||
TlvWriter()
|
||||
.put(TLVType.NICKNAME.value.toInt(), nicknameData, TlvLengthSize.ONE_BYTE)
|
||||
.put(TLVType.NOISE_PUBLIC_KEY.value.toInt(), noisePublicKey, TlvLengthSize.ONE_BYTE)
|
||||
.put(TLVType.SIGNING_PUBLIC_KEY.value.toInt(), signingPublicKey, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
val result = mutableListOf<Byte>()
|
||||
|
||||
// TLV for nickname
|
||||
result.add(TLVType.NICKNAME.value.toByte())
|
||||
result.add(nicknameData.size.toByte())
|
||||
result.addAll(nicknameData.toList())
|
||||
|
||||
// TLV for noise public key
|
||||
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
||||
result.add(noisePublicKey.size.toByte())
|
||||
result.addAll(noisePublicKey.toList())
|
||||
|
||||
// TLV for signing public key
|
||||
result.add(TLVType.SIGNING_PUBLIC_KEY.value.toByte())
|
||||
result.add(signingPublicKey.size.toByte())
|
||||
result.addAll(signingPublicKey.toList())
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -66,45 +56,29 @@ data class IdentityAnnouncement(
|
||||
* Decode from TLV binary data matching iOS implementation
|
||||
*/
|
||||
fun decode(data: ByteArray): IdentityAnnouncement? {
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
var offset = 0
|
||||
var nickname: String? = null
|
||||
var noisePublicKey: ByteArray? = null
|
||||
var signingPublicKey: ByteArray? = null
|
||||
|
||||
while (offset + 2 <= dataCopy.size) {
|
||||
// Read TLV type
|
||||
val typeValue = dataCopy[offset].toUByte()
|
||||
val type = TLVType.fromValue(typeValue)
|
||||
offset += 1
|
||||
|
||||
// Read TLV length
|
||||
val length = dataCopy[offset].toUByte().toInt()
|
||||
offset += 1
|
||||
|
||||
// Check bounds
|
||||
if (offset + length > dataCopy.size) return null
|
||||
|
||||
// Read TLV value
|
||||
val value = dataCopy.sliceArray(offset until offset + length)
|
||||
offset += length
|
||||
|
||||
// Process known TLV types, skip unknown ones for forward compatibility
|
||||
|
||||
val knownTypes = TLVType.values().map { it.value.toInt() }.toSet()
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = knownTypes
|
||||
) ?: return null
|
||||
|
||||
for (field in fields) {
|
||||
val type = TLVType.fromValue(field.type.toUByte()) ?: continue
|
||||
when (type) {
|
||||
TLVType.NICKNAME -> {
|
||||
nickname = String(value, Charsets.UTF_8)
|
||||
nickname = String(field.value, Charsets.UTF_8)
|
||||
}
|
||||
TLVType.NOISE_PUBLIC_KEY -> {
|
||||
noisePublicKey = value
|
||||
noisePublicKey = field.value
|
||||
}
|
||||
TLVType.SIGNING_PUBLIC_KEY -> {
|
||||
signingPublicKey = value
|
||||
}
|
||||
null -> {
|
||||
// Unknown TLV; skip (tolerant decoder for forward compatibility)
|
||||
continue
|
||||
signingPublicKey = field.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +114,6 @@ data class IdentityAnnouncement(
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "IdentityAnnouncement(nickname='$nickname', noisePublicKey=${noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., signingPublicKey=${signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...)"
|
||||
return "IdentityAnnouncement(nickname='$nickname', noisePublicKey=${noisePublicKey.toHexString().take(16)}..., signingPublicKey=${signingPublicKey.toHexString().take(16)}...)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
@@ -127,24 +131,14 @@ data class PrivateMessagePacket(
|
||||
val messageIDData = messageID.toByteArray(Charsets.UTF_8)
|
||||
val contentData = content.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Check size limits (TLV length field is 1 byte = max 255)
|
||||
if (messageIDData.size > 255 || contentData.size > 255) {
|
||||
return null
|
||||
return try {
|
||||
TlvWriter()
|
||||
.put(TLVType.MESSAGE_ID.value.toInt(), messageIDData, TlvLengthSize.ONE_BYTE)
|
||||
.put(TLVType.CONTENT.value.toInt(), contentData, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
val result = mutableListOf<Byte>()
|
||||
|
||||
// TLV for messageID
|
||||
result.add(TLVType.MESSAGE_ID.value.toByte())
|
||||
result.add(messageIDData.size.toByte())
|
||||
result.addAll(messageIDData.toList())
|
||||
|
||||
// TLV for content
|
||||
result.add(TLVType.CONTENT.value.toByte())
|
||||
result.add(contentData.size.toByte())
|
||||
result.addAll(contentData.toList())
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -152,33 +146,25 @@ data class PrivateMessagePacket(
|
||||
* Decode from TLV binary data - exactly like iOS
|
||||
*/
|
||||
fun decode(data: ByteArray): PrivateMessagePacket? {
|
||||
var offset = 0
|
||||
var messageID: String? = null
|
||||
var content: String? = null
|
||||
|
||||
while (offset + 2 <= data.size) {
|
||||
// Read TLV type
|
||||
val typeValue = data[offset].toUByte()
|
||||
val type = TLVType.fromValue(typeValue) ?: return null
|
||||
offset += 1
|
||||
|
||||
// Read TLV length
|
||||
val length = data[offset].toUByte().toInt()
|
||||
offset += 1
|
||||
|
||||
// Check bounds
|
||||
if (offset + length > data.size) return null
|
||||
|
||||
// Read TLV value
|
||||
val value = data.copyOfRange(offset, offset + length)
|
||||
offset += length
|
||||
|
||||
|
||||
val knownTypes = TLVType.values().map { it.value.toInt() }.toSet()
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.FAIL,
|
||||
knownTypes = knownTypes
|
||||
) ?: return null
|
||||
|
||||
for (field in fields) {
|
||||
val type = TLVType.fromValue(field.type.toUByte()) ?: return null
|
||||
when (type) {
|
||||
TLVType.MESSAGE_ID -> {
|
||||
messageID = String(value, Charsets.UTF_8)
|
||||
messageID = String(field.value, Charsets.UTF_8)
|
||||
}
|
||||
TLVType.CONTENT -> {
|
||||
content = String(value, Charsets.UTF_8)
|
||||
content = String(field.value, Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import com.bitchat.android.sync.SyncDefaults
|
||||
|
||||
/**
|
||||
@@ -15,30 +19,21 @@ data class RequestSyncPacket(
|
||||
val data: ByteArray
|
||||
) {
|
||||
fun encode(): ByteArray {
|
||||
val out = ArrayList<Byte>()
|
||||
fun putTLV(t: Int, v: ByteArray) {
|
||||
out.add(t.toByte())
|
||||
val len = v.size
|
||||
out.add(((len ushr 8) and 0xFF).toByte())
|
||||
out.add((len and 0xFF).toByte())
|
||||
out.addAll(v.toList())
|
||||
}
|
||||
// P
|
||||
putTLV(0x01, byteArrayOf(p.toByte()))
|
||||
// M (uint32)
|
||||
val m32 = m.coerceAtMost(0xffff_ffffL)
|
||||
putTLV(
|
||||
0x02,
|
||||
byteArrayOf(
|
||||
((m32 ushr 24) and 0xFF).toByte(),
|
||||
((m32 ushr 16) and 0xFF).toByte(),
|
||||
((m32 ushr 8) and 0xFF).toByte(),
|
||||
(m32 and 0xFF).toByte()
|
||||
return TlvWriter()
|
||||
.put(0x01, byteArrayOf(p.toByte()), TlvLengthSize.TWO_BYTES)
|
||||
.put(
|
||||
0x02,
|
||||
byteArrayOf(
|
||||
((m32 ushr 24) and 0xFF).toByte(),
|
||||
((m32 ushr 16) and 0xFF).toByte(),
|
||||
((m32 ushr 8) and 0xFF).toByte(),
|
||||
(m32 and 0xFF).toByte()
|
||||
),
|
||||
TlvLengthSize.TWO_BYTES
|
||||
)
|
||||
)
|
||||
// data
|
||||
putTLV(0x03, data)
|
||||
return out.toByteArray()
|
||||
.put(0x03, data, TlvLengthSize.TWO_BYTES)
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -46,28 +41,30 @@ data class RequestSyncPacket(
|
||||
const val MAX_ACCEPT_FILTER_BYTES: Int = SyncDefaults.MAX_ACCEPT_FILTER_BYTES
|
||||
|
||||
fun decode(data: ByteArray): RequestSyncPacket? {
|
||||
var off = 0
|
||||
var p: Int? = null
|
||||
var m: Long? = null
|
||||
var payload: ByteArray? = null
|
||||
|
||||
while (off + 3 <= data.size) {
|
||||
val t = (data[off].toInt() and 0xFF); off += 1
|
||||
val len = ((data[off].toInt() and 0xFF) shl 8) or (data[off+1].toInt() and 0xFF); off += 2
|
||||
if (off + len > data.size) return null
|
||||
val v = data.copyOfRange(off, off + len); off += len
|
||||
when (t) {
|
||||
0x01 -> if (len == 1) p = (v[0].toInt() and 0xFF)
|
||||
0x02 -> if (len == 4) {
|
||||
val mm = ((v[0].toLong() and 0xFF) shl 24) or
|
||||
((v[1].toLong() and 0xFF) shl 16) or
|
||||
((v[2].toLong() and 0xFF) shl 8) or
|
||||
(v[3].toLong() and 0xFF)
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.TWO_BYTES,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = setOf(0x01, 0x02, 0x03)
|
||||
) ?: return null
|
||||
|
||||
for (field in fields) {
|
||||
when (field.type) {
|
||||
0x01 -> if (field.value.size == 1) p = (field.value[0].toInt() and 0xFF)
|
||||
0x02 -> if (field.value.size == 4) {
|
||||
val mm = ((field.value[0].toLong() and 0xFF) shl 24) or
|
||||
((field.value[1].toLong() and 0xFF) shl 16) or
|
||||
((field.value[2].toLong() and 0xFF) shl 8) or
|
||||
(field.value[3].toLong() and 0xFF)
|
||||
m = mm
|
||||
}
|
||||
0x03 -> {
|
||||
if (v.size > MAX_ACCEPT_FILTER_BYTES) return null
|
||||
payload = v
|
||||
if (field.value.size > MAX_ACCEPT_FILTER_BYTES) return null
|
||||
payload = field.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user