feat: Implement file and avatar attachment handling in chat messages
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user