feat: implement debug logging functionality and UI for message processing
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user