feat: Implement file and avatar attachment handling in chat messages

This commit is contained in:
k1ngsterr1
2026-01-24 01:35:49 +05:00
parent 10c78e6231
commit ebfec3d0ba
8 changed files with 966 additions and 2 deletions

View File

@@ -515,3 +515,24 @@ class PacketPushNotification : Packet() {
return stream
}
}
/**
* Request Transport packet (ID: 0x0F)
* Запрос адреса транспортного сервера для загрузки/скачивания файлов
*/
class PacketRequestTransport : Packet() {
var transportServer: String = ""
override fun getPacketId(): Int = 0x0F
override fun receive(stream: Stream) {
transportServer = stream.readString()
}
override fun send(): Stream {
val stream = Stream()
stream.writeInt16(getPacketId())
stream.writeString(transportServer)
return stream
}
}

View File

@@ -174,6 +174,13 @@ object ProtocolManager {
}
}
}
// 🚀 Обработчик транспортного сервера (0x0F)
waitPacket(0x0F) { packet ->
val transportPacket = packet as PacketRequestTransport
android.util.Log.d("Protocol", "🚀 Transport server: ${transportPacket.transportServer}")
TransportManager.setTransportServer(transportPacket.transportServer)
}
}
/**
@@ -210,6 +217,12 @@ object ProtocolManager {
*/
fun authenticate(publicKey: String, privateHash: String) {
getProtocol().startHandshake(publicKey, privateHash)
// 🚀 Запрашиваем транспортный сервер после авторизации
scope.launch {
delay(500) // Даём время на завершение handshake
TransportManager.requestTransportServer()
}
}
/**

View File

@@ -0,0 +1,202 @@
package com.rosetta.messenger.network
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
/**
* Состояние загрузки/скачивания файла
*/
data class TransportState(
val id: String,
val progress: Int // 0-100
)
/**
* Менеджер транспорта файлов
* Загрузка и скачивание файлов с транспортного сервера
* Совместимо с desktop версией (TransportProvider)
*/
object TransportManager {
private const val TAG = "TransportManager"
private var transportServer: String? = null
private val _uploading = MutableStateFlow<List<TransportState>>(emptyList())
val uploading: StateFlow<List<TransportState>> = _uploading.asStateFlow()
private val _downloading = MutableStateFlow<List<TransportState>>(emptyList())
val downloading: StateFlow<List<TransportState>> = _downloading.asStateFlow()
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
/**
* Установить адрес транспортного сервера
*/
fun setTransportServer(server: String) {
transportServer = server
Log.d(TAG, "🚀 Transport server set: $server")
}
/**
* Получить адрес транспортного сервера
*/
fun getTransportServer(): String? = transportServer
/**
* Запросить адрес транспортного сервера с сервера протокола
*/
fun requestTransportServer() {
val packet = PacketRequestTransport()
ProtocolManager.sendPacket(packet)
}
/**
* Загрузить файл на транспортный сервер
* @param id Уникальный ID файла
* @param content Содержимое файла (base64 или бинарные данные)
* @return Tag для скачивания файла
*/
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
val server = transportServer
?: throw IllegalStateException("Transport server is not set")
Log.d(TAG, "📤 Uploading file: $id")
// Добавляем в список загрузок
_uploading.value = _uploading.value + TransportState(id, 0)
try {
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"file",
id,
content.toRequestBody("application/octet-stream".toMediaType())
)
.build()
val request = Request.Builder()
.url("$server/u")
.post(requestBody)
.build()
val response = suspendCoroutine<Response> { cont ->
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
cont.resume(response)
}
})
}
if (!response.isSuccessful) {
throw IOException("Upload failed: ${response.code}")
}
val responseBody = response.body?.string()
?: throw IOException("Empty response")
// Parse JSON response to get tag
val tag = org.json.JSONObject(responseBody).getString("t")
Log.d(TAG, "✅ Upload complete: $id -> $tag")
// Обновляем прогресс до 100%
_uploading.value = _uploading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
tag
} finally {
// Удаляем из списка загрузок
_uploading.value = _uploading.value.filter { it.id != id }
}
}
/**
* Скачать файл с транспортного сервера
* @param id Уникальный ID файла (для трекинга)
* @param tag Tag файла на сервере
* @return Содержимое файла
*/
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
val server = transportServer
?: throw IllegalStateException("Transport server is not set")
Log.d(TAG, "📥 Downloading file: $id (tag: $tag)")
// Добавляем в список скачиваний
_downloading.value = _downloading.value + TransportState(id, 0)
try {
val request = Request.Builder()
.url("$server/d/$tag")
.get()
.build()
val response = suspendCoroutine<Response> { cont ->
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
cont.resume(response)
}
})
}
if (!response.isSuccessful) {
throw IOException("Download failed: ${response.code}")
}
val content = response.body?.string()
?: throw IOException("Empty response")
Log.d(TAG, "✅ Download complete: $id (${content.length} bytes)")
// Обновляем прогресс до 100%
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
content
} finally {
// Удаляем из списка скачиваний
_downloading.value = _downloading.value.filter { it.id != id }
}
}
/**
* Получить прогресс скачивания файла
*/
fun getDownloadProgress(id: String): Int {
return _downloading.value.find { it.id == id }?.progress ?: -1
}
/**
* Получить прогресс загрузки файла
*/
fun getUploadProgress(id: String): Int {
return _uploading.value.find { it.id == id }?.progress ?: -1
}
}