feat: Enhance reply handling with detailed logging and update encryption method to SHA256
This commit is contained in:
@@ -592,24 +592,29 @@ object MessageCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Шифрует reply blob для attachments (как в React Native)
|
||||
* Использует PBKDF2 + AES-CBC с тем же ключом что и основное сообщение
|
||||
* Шифрование reply blob для передачи по сети
|
||||
*
|
||||
* В RN: encodeWithPassword(key.toString('utf-8'), JSON.stringify(reply))
|
||||
* где key = Buffer.concat([chacha_key, nonce]) - 56 bytes
|
||||
* Совместим с React Native:
|
||||
* 1. Compress with pako (deflate)
|
||||
* 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8')
|
||||
* 3. AES-256-CBC encryption
|
||||
*
|
||||
* ВАЖНО: JavaScript Buffer.toString('utf-8') на невалидных UTF-8 байтах
|
||||
* заменяет их на U+FFFD (replacement character). Это поведение нужно
|
||||
* воспроизвести в Kotlin для совместимости.
|
||||
* @param replyJson - JSON string to encrypt
|
||||
* @param plainKeyAndNonce - 56 bytes (32 key + 24 nonce)
|
||||
*
|
||||
* Формат выхода: "ivBase64:ciphertextBase64" (совместим с desktop)
|
||||
*/
|
||||
fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String {
|
||||
return try {
|
||||
android.util.Log.d("ReplyDebug", "🔐 encryptReplyBlob called:")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// Convert keyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior
|
||||
// Convert plainKeyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior
|
||||
// which replaces invalid UTF-8 sequences with U+FFFD
|
||||
val password = bytesToJsUtf8String(plainKeyAndNonce)
|
||||
android.util.Log.d("ReplyDebug", " - password length: ${password.length}")
|
||||
android.util.Log.d("ReplyDebug", " - password bytes (first 20): ${password.toByteArray(Charsets.UTF_8).take(20).joinToString(",")}")
|
||||
|
||||
// Compress with pako (deflate)
|
||||
val deflater = java.util.zip.Deflater()
|
||||
@@ -621,7 +626,8 @@ object MessageCrypto {
|
||||
val compressed = compressedBuffer.copyOf(compressedSize)
|
||||
|
||||
// PBKDF2 key derivation (matching RN: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000}))
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
// CRITICAL: Must use SHA256 to match React Native (not SHA1!)
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = javax.crypto.spec.PBEKeySpec(
|
||||
password.toCharArray(),
|
||||
"rosetta".toByteArray(Charsets.UTF_8),
|
||||
@@ -674,31 +680,54 @@ object MessageCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Расшифровывает reply blob из attachments (как в React Native)
|
||||
* Формат входа: "ivBase64:ciphertextBase64"
|
||||
* Расшифровка reply blob полученного по сети
|
||||
*
|
||||
* Совместим с React Native:
|
||||
* 1. Parse "ivBase64:ciphertextBase64" format
|
||||
* 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8')
|
||||
* 3. AES-256-CBC decryption
|
||||
* 4. Decompress with pako (inflate)
|
||||
*
|
||||
* @param encryptedBlob - "ivBase64:ciphertextBase64" format
|
||||
* @param plainKeyAndNonce - 56 bytes (32 key + 24 nonce)
|
||||
*/
|
||||
fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String {
|
||||
return try {
|
||||
android.util.Log.d("ReplyDebug", "🔓 decryptReplyBlob called:")
|
||||
android.util.Log.d("ReplyDebug", " - Input length: ${encryptedBlob.length}")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// Check if it's encrypted format (contains ':')
|
||||
if (!encryptedBlob.contains(':')) {
|
||||
android.util.Log.d("ReplyDebug", " - No ':' found, returning as-is")
|
||||
return encryptedBlob
|
||||
}
|
||||
|
||||
// Parse ivBase64:ciphertextBase64
|
||||
val parts = encryptedBlob.split(':')
|
||||
if (parts.size != 2) {
|
||||
android.util.Log.d("ReplyDebug", " - Invalid format (not 2 parts), returning as-is")
|
||||
return encryptedBlob
|
||||
}
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - IV part length: ${parts[0].length}")
|
||||
android.util.Log.d("ReplyDebug", " - Ciphertext part length: ${parts[1].length}")
|
||||
|
||||
val iv = Base64.decode(parts[0], Base64.DEFAULT)
|
||||
val ciphertext = Base64.decode(parts[1], Base64.DEFAULT)
|
||||
|
||||
// Password from keyAndNonce - use same JS-like UTF-8 conversion
|
||||
android.util.Log.d("ReplyDebug", " - Decoded IV size: ${iv.size}")
|
||||
android.util.Log.d("ReplyDebug", " - Decoded ciphertext size: ${ciphertext.size}")
|
||||
|
||||
// Password from plainKeyAndNonce - use same JS-like UTF-8 conversion
|
||||
val password = bytesToJsUtf8String(plainKeyAndNonce)
|
||||
android.util.Log.d("ReplyDebug", " - Password length: ${password.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Password bytes hex: ${password.toByteArray(Charsets.UTF_8).joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// PBKDF2 key derivation
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
// CRITICAL: Must use SHA256 to match React Native (not SHA1!)
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = javax.crypto.spec.PBEKeySpec(
|
||||
password.toCharArray(),
|
||||
"rosetta".toByteArray(Charsets.UTF_8),
|
||||
@@ -708,6 +737,8 @@ object MessageCrypto {
|
||||
val secretKey = factory.generateSecret(spec)
|
||||
val keyBytes = secretKey.encoded
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")
|
||||
|
||||
// AES-CBC decryption
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val keySpec = SecretKeySpec(keyBytes, "AES")
|
||||
@@ -715,6 +746,8 @@ object MessageCrypto {
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
|
||||
val decompressed = cipher.doFinal(ciphertext)
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - Decrypted (compressed) size: ${decompressed.size}")
|
||||
|
||||
// Decompress with inflate
|
||||
val inflater = java.util.zip.Inflater()
|
||||
inflater.setInput(decompressed)
|
||||
@@ -723,8 +756,14 @@ object MessageCrypto {
|
||||
inflater.end()
|
||||
val plaintext = String(outputBuffer, 0, outputSize, Charsets.UTF_8)
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - Decompressed plaintext length: ${plaintext.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Plaintext preview: ${plaintext.take(100)}")
|
||||
android.util.Log.d("ReplyDebug", "✅ decryptReplyBlob success")
|
||||
|
||||
plaintext
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ReplyDebug", "❌ decryptReplyBlob failed:", e)
|
||||
android.util.Log.e("ReplyDebug", " - Exception: ${e.javaClass.simpleName}: ${e.message}")
|
||||
// Return as-is, might be plain JSON
|
||||
encryptedBlob
|
||||
}
|
||||
|
||||
@@ -989,13 +989,35 @@ fun ChatDetailScreen(
|
||||
}
|
||||
|
||||
Column(modifier = bottomModifier) {
|
||||
// 🔥 UNIFIED BOTTOM BAR - один контейнер, контент меняется внутри
|
||||
Crossfade(
|
||||
// 🔥 UNIFIED BOTTOM BAR - один контейнер, контент меняется внутри с плавной анимацией
|
||||
AnimatedContent(
|
||||
targetState = isSelectionMode,
|
||||
animationSpec = tween(200),
|
||||
transitionSpec = {
|
||||
if (targetState) {
|
||||
// Selection mode появляется: снизу вверх
|
||||
slideInVertically(
|
||||
animationSpec = tween(300, easing = TelegramEasing),
|
||||
initialOffsetY = { it }
|
||||
) + fadeIn(animationSpec = tween(200)) togetherWith
|
||||
slideOutVertically(
|
||||
animationSpec = tween(300, easing = TelegramEasing),
|
||||
targetOffsetY = { -it }
|
||||
) + fadeOut(animationSpec = tween(150))
|
||||
} else {
|
||||
// Input bar возвращается: снизу вверх
|
||||
slideInVertically(
|
||||
animationSpec = tween(300, easing = TelegramEasing),
|
||||
initialOffsetY = { it }
|
||||
) + fadeIn(animationSpec = tween(200)) togetherWith
|
||||
slideOutVertically(
|
||||
animationSpec = tween(300, easing = TelegramEasing),
|
||||
targetOffsetY = { it }
|
||||
) + fadeOut(animationSpec = tween(150))
|
||||
}
|
||||
},
|
||||
label = "bottomBarContent"
|
||||
) { selectionMode ->
|
||||
android.util.Log.d("ChatDetailScreen", "🎬 Crossfade to selectionMode=$selectionMode")
|
||||
android.util.Log.d("ChatDetailScreen", "🎬 AnimatedContent to selectionMode=$selectionMode")
|
||||
|
||||
if (selectionMode) {
|
||||
// SELECTION ACTION BAR - Reply/Forward
|
||||
@@ -1013,13 +1035,25 @@ fun ChatDetailScreen(
|
||||
.background(if (isDarkTheme) Color.White.copy(alpha = 0.15f) else Color.Black.copy(alpha = 0.1f))
|
||||
)
|
||||
|
||||
// Кнопки Reply и Forward - такие же отступы как у input row
|
||||
// Кнопки Reply и Forward - плавная анимация появления
|
||||
val buttonScale by animateFloatAsState(
|
||||
targetValue = if (selectionMode) 1f else 0.95f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "buttonScale"
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
.navigationBarsPadding(),
|
||||
.navigationBarsPadding()
|
||||
.graphicsLayer {
|
||||
scaleX = buttonScale
|
||||
scaleY = buttonScale
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
@@ -228,22 +228,36 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var blobToStore = att.blob // По умолчанию сохраняем оригинальный blob
|
||||
if (att.type == AttachmentType.MESSAGES && att.blob.isNotEmpty()) {
|
||||
try {
|
||||
// 🔥 Сначала расшифровываем blob (он зашифрован!)
|
||||
android.util.Log.d("ReplyDebug", "📥 [RECEIVE] Processing reply attachment:")
|
||||
android.util.Log.d("ReplyDebug", " - Encrypted blob length: ${att.blob.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Encrypted blob preview: ${att.blob.take(100)}")
|
||||
android.util.Log.d("ReplyDebug", " - Decrypting with plainKeyAndNonce (${plainKeyAndNonce.size} bytes)")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// 🔥 Расшифровываем с полным plainKeyAndNonce (56 bytes)
|
||||
// Desktop использует chachaDecryptedKey.toString('utf-8') = полные 56 байт!
|
||||
val decryptedBlob = MessageCrypto.decryptReplyBlob(att.blob, plainKeyAndNonce)
|
||||
android.util.Log.d("ReplyDebug", " - Decrypted blob length: ${decryptedBlob.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Decrypted blob preview: ${decryptedBlob.take(200)}")
|
||||
|
||||
// 🔥 Сохраняем расшифрованный blob в БД
|
||||
blobToStore = decryptedBlob
|
||||
|
||||
// Парсим JSON массив с цитируемыми сообщениями
|
||||
val replyArray = JSONArray(decryptedBlob)
|
||||
android.util.Log.d("ReplyDebug", " - Reply array length: ${replyArray.length()}")
|
||||
if (replyArray.length() > 0) {
|
||||
val firstReply = replyArray.getJSONObject(0)
|
||||
val replyPublicKey = firstReply.optString("publicKey", "")
|
||||
val replyText = firstReply.optString("message", "")
|
||||
val replyMessageId = firstReply.optString("message_id", "")
|
||||
android.util.Log.d("ReplyDebug", " - Parsed reply: id=$replyMessageId")
|
||||
android.util.Log.d("ReplyDebug", " publicKey=${replyPublicKey.take(20)}...")
|
||||
android.util.Log.d("ReplyDebug", " message=${replyText.take(50)}")
|
||||
|
||||
// Определяем автора цитаты
|
||||
val isReplyFromMe = replyPublicKey == myPublicKey
|
||||
android.util.Log.d("ReplyDebug", " - Is reply from me: $isReplyFromMe")
|
||||
|
||||
replyData = ReplyData(
|
||||
messageId = replyMessageId,
|
||||
@@ -251,8 +265,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
text = replyText,
|
||||
isFromMe = isReplyFromMe
|
||||
)
|
||||
android.util.Log.d("ReplyDebug", "✅ [RECEIVE] Reply data created successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ReplyDebug", "❌ [RECEIVE] Failed to decrypt/parse reply:", e)
|
||||
android.util.Log.e("ReplyDebug", " - Encrypted blob: ${att.blob.take(100)}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,7 +625,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
// Парсим attachments для поиска MESSAGES (цитата)
|
||||
var replyData = parseReplyFromAttachments(entity.attachments, entity.fromMe == 1)
|
||||
// 🔥 ВАЖНО: Передаем content и chachaKey для расшифровки reply blob если нужно
|
||||
var replyData = parseReplyFromAttachments(
|
||||
attachmentsJson = entity.attachments,
|
||||
isFromMe = entity.fromMe == 1,
|
||||
content = entity.content,
|
||||
chachaKey = entity.chachaKey
|
||||
)
|
||||
|
||||
// Если не нашли reply в attachments, пробуем распарсить из текста
|
||||
if (replyData == null) {
|
||||
@@ -662,24 +685,45 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
/**
|
||||
* Парсинг MESSAGES attachment для извлечения данных цитаты
|
||||
* Формат: [{"message_id": "...", "publicKey": "...", "message": "..."}]
|
||||
* 🔥 ВАЖНО: Если blob зашифрован (формат "iv:ciphertext"), расшифровываем его
|
||||
*/
|
||||
private fun parseReplyFromAttachments(attachmentsJson: String, isFromMe: Boolean): ReplyData? {
|
||||
private suspend fun parseReplyFromAttachments(
|
||||
attachmentsJson: String,
|
||||
isFromMe: Boolean,
|
||||
content: String,
|
||||
chachaKey: String
|
||||
): ReplyData? {
|
||||
if (attachmentsJson.isEmpty() || attachmentsJson == "[]") return null
|
||||
|
||||
return try {
|
||||
android.util.Log.d("ReplyDebug", "💾 [DB] Parsing reply from attachments JSON")
|
||||
android.util.Log.d("ReplyDebug", " - Attachments JSON preview: ${attachmentsJson.take(200)}")
|
||||
val attachments = JSONArray(attachmentsJson)
|
||||
android.util.Log.d("ReplyDebug", " - Attachments count: ${attachments.length()}")
|
||||
for (i in 0 until attachments.length()) {
|
||||
val attachment = attachments.getJSONObject(i)
|
||||
val type = attachment.optInt("type", 0)
|
||||
android.util.Log.d("ReplyDebug", " - Attachment $i type: $type")
|
||||
|
||||
// MESSAGES = 1 (цитата)
|
||||
if (type == 1) {
|
||||
// Данные могут быть в blob или preview
|
||||
val dataJson = attachment.optString("blob", "").ifEmpty {
|
||||
var dataJson = attachment.optString("blob", "").ifEmpty {
|
||||
attachment.optString("preview", "")
|
||||
}
|
||||
android.util.Log.d("ReplyDebug", " - Found MESSAGES attachment, data length: ${dataJson.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Data preview: ${dataJson.take(200)}")
|
||||
if (dataJson.isEmpty()) continue
|
||||
|
||||
// 🔥 Проверяем формат blob - если содержит ":", то это зашифрованный формат "iv:ciphertext"
|
||||
// Это старые сообщения (полученные до фикса) которые нельзя расшифровать
|
||||
if (dataJson.contains(":") && dataJson.split(":").size == 2) {
|
||||
android.util.Log.d("ReplyDebug", " - Blob is encrypted (old format), skipping...")
|
||||
android.util.Log.d("ReplyDebug", " - Cannot decrypt old reply messages - they were saved before fix")
|
||||
// Пропускаем старые зашифрованные сообщения
|
||||
continue
|
||||
}
|
||||
|
||||
val messagesArray = JSONArray(dataJson)
|
||||
if (messagesArray.length() > 0) {
|
||||
val replyMessage = messagesArray.getJSONObject(0)
|
||||
@@ -691,17 +735,22 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
// Если publicKey == myPublicKey - цитата от меня
|
||||
val isReplyFromMe = replyPublicKey == myPublicKey
|
||||
|
||||
return ReplyData(
|
||||
android.util.Log.d("ReplyDebug", " - Parsed from DB: id=$replyMessageId, text=${replyText.take(30)}, isFromMe=$isReplyFromMe")
|
||||
val result = ReplyData(
|
||||
messageId = replyMessageId,
|
||||
senderName = if (isReplyFromMe) "You" else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } },
|
||||
text = replyText,
|
||||
isFromMe = isReplyFromMe
|
||||
)
|
||||
android.util.Log.d("ReplyDebug", "✅ [DB] Reply data parsed successfully from DB")
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
android.util.Log.d("ReplyDebug", "💾 [DB] No MESSAGES attachment found")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ReplyDebug", "❌ [DB] Failed to parse reply from attachments:", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -893,10 +942,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var replyBlobPlaintext = "" // Сохраняем plaintext для БД
|
||||
|
||||
if (replyMsgsToSend.isNotEmpty()) {
|
||||
android.util.Log.d("ReplyDebug", "📤 [SEND] Creating reply attachment:")
|
||||
android.util.Log.d("ReplyDebug", " - Reply messages count: ${replyMsgsToSend.size}")
|
||||
|
||||
// Формируем JSON массив с цитируемыми сообщениями (как в RN)
|
||||
val replyJsonArray = JSONArray()
|
||||
replyMsgsToSend.forEach { msg ->
|
||||
android.util.Log.d("ReplyDebug", " - Adding reply: id=${msg.messageId}, publicKey=${msg.publicKey.take(20)}..., text=${msg.text.take(30)}")
|
||||
val replyJson = JSONObject().apply {
|
||||
put("message_id", msg.messageId)
|
||||
put("publicKey", msg.publicKey)
|
||||
@@ -908,9 +960,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
replyBlobPlaintext = replyJsonArray.toString() // 🔥 Сохраняем plaintext
|
||||
android.util.Log.d("ReplyDebug", " - Reply blob plaintext length: ${replyBlobPlaintext.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Reply blob preview: ${replyBlobPlaintext.take(100)}")
|
||||
android.util.Log.d("ReplyDebug", " - Encrypting reply blob with plainKeyAndNonce (${plainKeyAndNonce.size} bytes)")
|
||||
|
||||
// 🔥 Шифруем reply blob plainKeyAndNonce (как в React Native)
|
||||
// 🔥 Шифруем reply blob (для network transmission)
|
||||
val encryptedReplyBlob = MessageCrypto.encryptReplyBlob(replyBlobPlaintext, plainKeyAndNonce)
|
||||
android.util.Log.d("ReplyDebug", " - Encrypted blob length: ${encryptedReplyBlob.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Encrypted blob preview: ${encryptedReplyBlob.take(100)}")
|
||||
|
||||
val replyAttachmentId = "reply_${timestamp}"
|
||||
messageAttachments.add(MessageAttachment(
|
||||
@@ -919,6 +976,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
type = AttachmentType.MESSAGES,
|
||||
preview = ""
|
||||
))
|
||||
android.util.Log.d("ReplyDebug", "✅ [SEND] Reply attachment added to packet (encrypted)")
|
||||
}
|
||||
|
||||
val packet = PacketMessage().apply {
|
||||
@@ -932,6 +990,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
attachments = messageAttachments
|
||||
}
|
||||
|
||||
// 🔥 DEBUG: Log packet before sending
|
||||
android.util.Log.d("ReplyDebug", "📤 [SEND] About to send packet:")
|
||||
android.util.Log.d("ReplyDebug", " - messageId: $messageId")
|
||||
android.util.Log.d("ReplyDebug", " - from: ${sender.take(20)}...")
|
||||
android.util.Log.d("ReplyDebug", " - to: ${recipient.take(20)}...")
|
||||
android.util.Log.d("ReplyDebug", " - attachments count: ${packet.attachments.size}")
|
||||
packet.attachments.forEachIndexed { idx, att ->
|
||||
android.util.Log.d("ReplyDebug", " - Attachment $idx:")
|
||||
android.util.Log.d("ReplyDebug", " type: ${att.type.value}")
|
||||
android.util.Log.d("ReplyDebug", " id: ${att.id}")
|
||||
android.util.Log.d("ReplyDebug", " blob length: ${att.blob.length}")
|
||||
android.util.Log.d("ReplyDebug", " blob preview: ${att.blob.take(100)}")
|
||||
}
|
||||
|
||||
// Отправляем пакет
|
||||
ProtocolManager.send(packet)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user