feat: implement debug logging functionality and UI for message processing

This commit is contained in:
k1ngsterr1
2026-02-06 00:21:11 +05:00
parent 718eb4ef56
commit c455994224
9 changed files with 861 additions and 107 deletions

View File

@@ -7,6 +7,7 @@ import com.rosetta.messenger.database.*
import com.rosetta.messenger.network.*
import com.rosetta.messenger.utils.AttachmentFileManager
import com.rosetta.messenger.utils.AvatarFileManager
import com.rosetta.messenger.utils.MessageLogger
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.json.JSONArray
@@ -84,7 +85,29 @@ class MessageRepository private constructor(private val context: Context) {
companion object {
@Volatile
private var INSTANCE: MessageRepository? = null
// 🔥 In-memory кэш обработанных messageId для предотвращения дубликатов
// LRU кэш с ограничением 1000 элементов - защита от race conditions
private val processedMessageIds = java.util.Collections.synchronizedSet(
object : LinkedHashSet<String>() {
override fun add(element: String): Boolean {
if (size >= 1000) remove(first())
return super.add(element)
}
}
)
/**
* Помечает messageId как обработанный и возвращает true если это новый ID
* Возвращает false если сообщение уже было обработано (дубликат)
*/
fun markAsProcessed(messageId: String): Boolean = processedMessageIds.add(messageId)
/**
* Очистка кэша (вызывается при logout)
*/
fun clearProcessedCache() = processedMessageIds.clear()
fun getInstance(context: Context): MessageRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MessageRepository(context.applicationContext).also { INSTANCE = it }
@@ -175,6 +198,16 @@ class MessageRepository private constructor(private val context: Context) {
// 📁 Проверяем является ли это Saved Messages
val isSavedMessages = (account == toPublicKey)
// 📝 LOG: Начало отправки
MessageLogger.logSendStart(
messageId = messageId,
toPublicKey = toPublicKey,
textLength = text.trim().length,
attachmentsCount = attachments.size,
isSavedMessages = isSavedMessages,
replyToMessageId = replyToMessageId
)
// 1. Создаем оптимистичное сообщение
// 📁 Для saved messages - сразу DELIVERED и прочитано
val optimisticMessage = Message(
@@ -192,9 +225,11 @@ class MessageRepository private constructor(private val context: Context) {
// 2. Обновляем UI сразу (Optimistic Update)
updateMessageCache(dialogKey, optimisticMessage)
MessageLogger.logCacheUpdate(dialogKey, messageCache[dialogKey]?.value?.size ?: 0)
// 3. Фоновая обработка
scope.launch {
val startTime = System.currentTimeMillis()
try {
// Шифрование
val encryptResult = MessageCrypto.encryptForSending(
@@ -204,6 +239,13 @@ class MessageRepository private constructor(private val context: Context) {
val encryptedContent = encryptResult.ciphertext
val encryptedKey = encryptResult.encryptedKey
// 📝 LOG: Шифрование успешно
MessageLogger.logEncryptionSuccess(
messageId = messageId,
encryptedContentLength = encryptedContent.length,
encryptedKeyLength = encryptedKey.length
)
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
// Desktop хранит зашифрованный ключ, расшифровывает только при использовании
@@ -236,6 +278,10 @@ class MessageRepository private constructor(private val context: Context) {
)
messageDao.insertMessage(entity)
// 📝 LOG: Сохранено в БД
MessageLogger.logDbSave(messageId, dialogKey, isNew = true)
} else {
MessageLogger.logDbSave(messageId, dialogKey, isNew = false)
}
// 🔥 КРИТИЧНО: Обновляем диалог через updateDialogFromMessages
@@ -243,6 +289,11 @@ class MessageRepository private constructor(private val context: Context) {
// 🔥 Логируем что записалось в диалог
val dialog = dialogDao.getDialog(account, toPublicKey)
MessageLogger.logDialogUpdate(
dialogKey = dialogKey,
lastMessage = dialog?.lastMessage,
unreadCount = dialog?.unreadCount ?: 0
)
// 🔥 Отмечаем что я отправлял сообщения в этот диалог (перемещает из requests в chats)
val updatedRows = dialogDao.markIHaveSent(account, toPublicKey)
@@ -250,6 +301,8 @@ class MessageRepository private constructor(private val context: Context) {
// 📁 НЕ отправляем пакет на сервер для saved messages!
// Как в Архиве: if(publicKey == opponentPublicKey) return;
if (isSavedMessages) {
MessageLogger.logSendSuccess(messageId, System.currentTimeMillis() - startTime)
MessageLogger.debug("📁 SavedMessages: skipping server send")
return@launch // Для saved messages - только локальное сохранение, без отправки на сервер
}
@@ -265,9 +318,18 @@ class MessageRepository private constructor(private val context: Context) {
this.attachments = attachments
}
// 📝 LOG: Отправка пакета
MessageLogger.logPacketSend(messageId, toPublicKey, timestamp)
ProtocolManager.send(packet)
// 📝 LOG: Успешная отправка
MessageLogger.logSendSuccess(messageId, System.currentTimeMillis() - startTime)
} catch (e: Exception) {
// 📝 LOG: Ошибка отправки
MessageLogger.logSendError(messageId, e)
// При ошибке обновляем статус
messageDao.updateDeliveryStatus(account, messageId, DeliveryStatus.ERROR.value)
updateMessageStatus(dialogKey, messageId, DeliveryStatus.ERROR)
@@ -281,29 +343,53 @@ class MessageRepository private constructor(private val context: Context) {
* Обработка входящего сообщения
*/
suspend fun handleIncomingMessage(packet: PacketMessage) {
val startTime = System.currentTimeMillis()
val account = currentAccount ?: run {
MessageLogger.debug("📥 RECEIVE SKIP: account is null")
return
}
val privateKey = currentPrivateKey ?: run {
MessageLogger.debug("📥 RECEIVE SKIP: privateKey is null")
return
}
// 📝 LOG: Начало обработки входящего сообщения
MessageLogger.logReceiveStart(
messageId = packet.messageId,
fromPublicKey = packet.fromPublicKey,
toPublicKey = packet.toPublicKey,
contentLength = packet.content.length,
attachmentsCount = packet.attachments.size,
timestamp = packet.timestamp
)
// 🔥 Проверяем, не заблокирован ли отправитель
val isBlocked = database.blacklistDao().isUserBlocked(packet.fromPublicKey, account)
if (isBlocked) {
MessageLogger.logBlockedSender(packet.fromPublicKey)
return
}
// 🔥 Генерируем messageId если он пустой (как в Архиве - generateRandomKeyFormSeed)
val messageId = if (packet.messageId.isBlank()) {
generateMessageId(packet.fromPublicKey, packet.toPublicKey, packet.timestamp)
val generatedId = generateMessageId(packet.fromPublicKey, packet.toPublicKey, packet.timestamp)
MessageLogger.debug("📥 Generated messageId: $generatedId (original was blank)")
generatedId
} else {
packet.messageId
}
// Проверяем, не дубликат ли (используем сгенерированный messageId)
// 🔥 ПЕРВЫЙ УРОВЕНЬ ЗАЩИТЫ: In-memory кэш (быстрая проверка без обращения к БД)
// markAsProcessed возвращает false если сообщение уже обрабатывалось
if (!markAsProcessed(messageId)) {
MessageLogger.debug("📥 SKIP (in-memory cache): Message $messageId already being processed")
return
}
// 🔥 ВТОРОЙ УРОВЕНЬ ЗАЩИТЫ: Проверка в БД (для сообщений сохранённых в предыдущих сессиях)
val isDuplicate = messageDao.messageExists(account, messageId)
MessageLogger.logDuplicateCheck(messageId, isDuplicate)
if (isDuplicate) {
return
}
@@ -322,6 +408,13 @@ class MessageRepository private constructor(private val context: Context) {
privateKey
)
// 📝 LOG: Расшифровка успешна
MessageLogger.logDecryptionSuccess(
messageId = messageId,
plainTextLength = plainText.length,
attachmentsCount = packet.attachments.size
)
// Сериализуем attachments в JSON с расшифровкой MESSAGES blob
val attachmentsJson = serializeAttachmentsWithDecryption(
packet.attachments,
@@ -335,7 +428,7 @@ class MessageRepository private constructor(private val context: Context) {
// 📸 Обрабатываем AVATAR attachments - сохраняем аватар отправителя
processAvatarAttachments(packet.attachments, packet.fromPublicKey, packet.chachaKey, privateKey)
// <EFBFBD>🔒 Шифруем plainMessage с использованием приватного ключа
// 🔒 Шифруем plainMessage с использованием приватного ключа
val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey)
// Создаем entity для кэша и возможной вставки
@@ -361,7 +454,9 @@ class MessageRepository private constructor(private val context: Context) {
if (!stillExists) {
// Сохраняем в БД только если сообщения нет
messageDao.insertMessage(entity)
MessageLogger.logDbSave(messageId, dialogKey, isNew = true)
} else {
MessageLogger.logDbSave(messageId, dialogKey, isNew = false)
}
// 🔥 КРИТИЧНО: Обновляем диалог через updateDialogFromMessages
@@ -369,6 +464,11 @@ class MessageRepository private constructor(private val context: Context) {
// 🔥 Логируем что записалось в диалог
val dialog = dialogDao.getDialog(account, packet.fromPublicKey)
MessageLogger.logDialogUpdate(
dialogKey = dialogKey,
lastMessage = dialog?.lastMessage,
unreadCount = dialog?.unreadCount ?: 0
)
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
requestUserInfo(packet.fromPublicKey)
@@ -377,12 +477,18 @@ class MessageRepository private constructor(private val context: Context) {
if (!stillExists) {
val message = entity.toMessage()
updateMessageCache(dialogKey, message)
MessageLogger.logCacheUpdate(dialogKey, messageCache[dialogKey]?.value?.size ?: 0)
// 🔔 Уведомляем UI о новом сообщении (emit dialogKey для обновления)
_newMessageEvents.tryEmit(dialogKey)
}
// 📝 LOG: Успешная обработка
MessageLogger.logReceiveSuccess(messageId, System.currentTimeMillis() - startTime)
} catch (e: Exception) {
// 📝 LOG: Ошибка обработки
MessageLogger.logDecryptionError(messageId, e)
e.printStackTrace()
}
}
@@ -392,6 +498,14 @@ class MessageRepository private constructor(private val context: Context) {
*/
suspend fun handleDelivery(packet: PacketDelivery) {
val account = currentAccount ?: return
// 📝 LOG: Получено подтверждение доставки
MessageLogger.logDeliveryStatus(
messageId = packet.messageId,
toPublicKey = packet.toPublicKey,
status = "DELIVERED"
)
messageDao.updateDeliveryStatus(account, packet.messageId, DeliveryStatus.DELIVERED.value)
// Обновляем кэш
@@ -410,6 +524,7 @@ class MessageRepository private constructor(private val context: Context) {
suspend fun handleRead(packet: PacketRead) {
val account = currentAccount ?: return
MessageLogger.debug("👁 READ PACKET from: ${packet.fromPublicKey.take(16)}...")
// Проверяем последнее сообщение ДО обновления
val lastMsgBefore = messageDao.getLastMessageDebug(account, packet.fromPublicKey)
@@ -422,6 +537,7 @@ class MessageRepository private constructor(private val context: Context) {
// Обновляем кэш - все исходящие сообщения помечаем как прочитанные
val dialogKey = getDialogKey(packet.fromPublicKey)
val readCount = messageCache[dialogKey]?.value?.count { it.isFromMe && !it.isRead } ?: 0
messageCache[dialogKey]?.let { flow ->
flow.value = flow.value.map { msg ->
if (msg.isFromMe && !msg.isRead) msg.copy(isRead = true)
@@ -429,6 +545,12 @@ class MessageRepository private constructor(private val context: Context) {
}
}
// 📝 LOG: Статус прочтения
MessageLogger.logReadStatus(
fromPublicKey = packet.fromPublicKey,
messagesCount = readCount
)
// 🔥 КРИТИЧНО: Обновляем диалог чтобы lastMessageRead обновился
dialogDao.updateDialogFromMessages(account, packet.fromPublicKey)