mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-03 12:57:32 +01:00
adding support for sending clear images
This commit is contained in:
parent
3859aa7f0b
commit
0730cd069c
@ -2,8 +2,11 @@ package app.dapk.st.graph
|
||||
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import app.dapk.db.DapkDb
|
||||
import app.dapk.st.BuildConfig
|
||||
@ -33,6 +36,7 @@ import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||
import app.dapk.st.matrix.message.MessageEncrypter
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.installMessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.push.installPushService
|
||||
import app.dapk.st.matrix.push.pushService
|
||||
@ -86,10 +90,10 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||
private val workModule = WorkModule(context)
|
||||
private val imageLoaderModule = ImageLoaderModule(context)
|
||||
|
||||
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers)
|
||||
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver)
|
||||
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker)
|
||||
|
||||
val coreAndroidModule = app.dapk.st.core.CoreAndroidModule(intentFactory = object : IntentFactory {
|
||||
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
|
||||
override fun notificationOpenApp(context: Context) = PendingIntent.getActivity(
|
||||
context,
|
||||
1000,
|
||||
@ -137,7 +141,7 @@ internal class FeatureModules internal constructor(
|
||||
private val domainModules: DomainModules,
|
||||
private val trackingModule: TrackingModule,
|
||||
private val workModule: WorkModule,
|
||||
private val coreAndroidModule: app.dapk.st.core.CoreAndroidModule,
|
||||
private val coreAndroidModule: CoreAndroidModule,
|
||||
imageLoaderModule: ImageLoaderModule,
|
||||
context: Context,
|
||||
buildMeta: BuildMeta,
|
||||
@ -213,6 +217,7 @@ internal class MatrixModules(
|
||||
private val workModule: WorkModule,
|
||||
private val logger: MatrixLogger,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val contentResolver: ContentResolver,
|
||||
) {
|
||||
|
||||
val matrix by unsafeLazy {
|
||||
@ -253,7 +258,8 @@ internal class MatrixModules(
|
||||
base64 = base64,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler())) { serviceProvider ->
|
||||
val imageContentReader = AndroidImageContentReader(contentResolver)
|
||||
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader) { serviceProvider ->
|
||||
MessageEncrypter { message ->
|
||||
val result = serviceProvider.cryptoService().encrypt(
|
||||
roomId = when (message) {
|
||||
@ -273,6 +279,7 @@ internal class MatrixModules(
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageService.Message.ImageMessage -> TODO()
|
||||
}
|
||||
)
|
||||
@ -344,12 +351,14 @@ internal class MatrixModules(
|
||||
apiEvent.content.methods,
|
||||
apiEvent.content.timestampPosix,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.methods,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
@ -360,6 +369,7 @@ internal class MatrixModules(
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||
@ -367,6 +377,7 @@ internal class MatrixModules(
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.key
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
@ -417,3 +428,24 @@ internal class DomainModules(
|
||||
val pushModule by unsafeLazy { PushModule(matrixModules.push, errorTracker) }
|
||||
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
|
||||
}
|
||||
|
||||
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
|
||||
override fun read(uri: String): ImageContentReader.ImageContent {
|
||||
val androidUri = Uri.parse(uri)
|
||||
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
|
||||
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeStream(fileStream, null, options)
|
||||
|
||||
return contentResolver.openInputStream(androidUri)?.use { stream ->
|
||||
val output = stream.readBytes()
|
||||
ImageContentReader.ImageContent(
|
||||
height = options.outHeight,
|
||||
width = options.outWidth,
|
||||
size = output.size.toLong(),
|
||||
fileName = androidUri.lastPathSegment ?: "file",
|
||||
content = output
|
||||
)
|
||||
} ?: throw IllegalArgumentException("Could not process $uri")
|
||||
}
|
||||
}
|
@ -58,6 +58,7 @@ class LocalEchoPersistence(
|
||||
database.transaction {
|
||||
when (message) {
|
||||
is MessageService.Message.TextMessage -> database.localEchoQueries.delete(message.localId)
|
||||
is MessageService.Message.ImageMessage -> database.localEchoQueries.delete(message.localId)
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
@ -86,6 +87,14 @@ class LocalEchoPersistence(
|
||||
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageService.Message.ImageMessage -> database.localEchoQueries.insert(
|
||||
DbLocalEcho(
|
||||
message.localId,
|
||||
message.roomId.value,
|
||||
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import app.dapk.st.matrix.sync.RoomEvent
|
||||
|
||||
internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||
|
||||
fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent.Message {
|
||||
fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent {
|
||||
return when (val message = this.message) {
|
||||
is MessageService.Message.TextMessage -> {
|
||||
RoomEvent.Message(
|
||||
@ -19,7 +19,15 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||
meta = metaMapper.toMeta(this)
|
||||
)
|
||||
}
|
||||
is MessageService.Message.ImageMessage -> TODO()
|
||||
is MessageService.Message.ImageMessage -> {
|
||||
RoomEvent.Image(
|
||||
eventId = this.eventId ?: EventId(this.localId),
|
||||
author = member,
|
||||
utcTimestamp = message.timestampUtc,
|
||||
meta = metaMapper.toMeta(this),
|
||||
imageMeta = RoomEvent.Image.ImageMeta(100, 100, message.content.uri, null),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ internal class MergeWithLocalEchosUseCaseImpl(
|
||||
return roomState.copy(events = sortedEvents)
|
||||
}
|
||||
|
||||
private fun uniqueEchos(echos: List<MessageService.LocalEcho>, stateByEventId: Map<EventId, RoomEvent>, member: RoomMember): List<RoomEvent.Message> {
|
||||
private fun uniqueEchos(echos: List<MessageService.LocalEcho>, stateByEventId: Map<EventId, RoomEvent>, member: RoomMember): List<RoomEvent> {
|
||||
return with(localEventMapper) {
|
||||
echos
|
||||
.filter { echo -> echo.eventId == null || stateByEventId[echo.eventId] == null }
|
||||
|
@ -109,7 +109,7 @@ internal class MessengerViewModel(
|
||||
viewModelScope.launch {
|
||||
messageService.scheduleMessage(
|
||||
MessageService.Message.ImageMessage(
|
||||
MessageService.Message.Content.ImageContent(uri = copy.values.first().uri.value),
|
||||
MessageService.Message.Content.ApiImageContent(uri = copy.values.first().uri.value),
|
||||
roomId = roomState.roomOverview.roomId,
|
||||
sendEncrypted = roomState.roomOverview.isEncrypted,
|
||||
localId = localIdFactory.create(),
|
||||
|
@ -71,7 +71,7 @@ class MessengerViewModelTest {
|
||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
|
||||
|
||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID))
|
||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, emptyList()))
|
||||
|
||||
assertStates<MessengerScreenState>(
|
||||
{ copy(roomId = A_ROOM_ID) },
|
||||
|
@ -51,7 +51,6 @@ class NotificationRendererTest {
|
||||
}
|
||||
|
||||
notificationRenderer.render(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet()))
|
||||
|
||||
verifyExpects()
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ private fun Content(listState: LazyListState, state: Content, onClick: (Item) ->
|
||||
|
||||
@Composable
|
||||
private fun DirectoryItem(item: Item, onClick: (Item) -> Unit) {
|
||||
val roomName = item.roomName
|
||||
val roomName = item.roomName.ifEmpty { "Empty " }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
|
@ -1,10 +1,16 @@
|
||||
package app.dapk.st.matrix.message
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.MxUrl
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ApiSendResponse(
|
||||
@SerialName("event_id") val eventId: EventId,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiUploadResponse(
|
||||
@SerialName("content_uri") val contentUri: MxUrl,
|
||||
)
|
@ -4,11 +4,9 @@ import app.dapk.st.matrix.MatrixService
|
||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
||||
import app.dapk.st.matrix.MatrixServiceProvider
|
||||
import app.dapk.st.matrix.ServiceDepFactory
|
||||
import app.dapk.st.matrix.common.AlgorithmName
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.MessageType
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.message.internal.DefaultMessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -49,7 +47,7 @@ interface MessageService : MatrixService {
|
||||
@Serializable
|
||||
@SerialName("image_message")
|
||||
data class ImageMessage(
|
||||
@SerialName("content") val content: Content.ImageContent,
|
||||
@SerialName("content") val content: Content.ApiImageContent,
|
||||
@SerialName("send_encrypted") val sendEncrypted: Boolean,
|
||||
@SerialName("room_id") val roomId: RoomId,
|
||||
@SerialName("local_id") val localId: String,
|
||||
@ -65,10 +63,25 @@ interface MessageService : MatrixService {
|
||||
) : Content()
|
||||
|
||||
@Serializable
|
||||
data class ImageContent(
|
||||
data class ApiImageContent(
|
||||
@SerialName("uri") val uri: String,
|
||||
@SerialName("msgtype") val type: String = MessageType.IMAGE.value,
|
||||
) : Content()
|
||||
|
||||
@Serializable
|
||||
data class ImageContent(
|
||||
@SerialName("url") val url: MxUrl,
|
||||
@SerialName("body") val filename: String,
|
||||
@SerialName("info") val info: Info,
|
||||
@SerialName("msgtype") val type: String = MessageType.IMAGE.value,
|
||||
) : Content() {
|
||||
|
||||
@Serializable
|
||||
data class Info(
|
||||
@SerialName("h") val height: Int,
|
||||
@SerialName("w") val width: Int,
|
||||
@SerialName("size") val size: Long,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,10 +140,11 @@ interface MessageService : MatrixService {
|
||||
fun MatrixServiceInstaller.installMessageService(
|
||||
localEchoStore: LocalEchoStore,
|
||||
backgroundScheduler: BackgroundScheduler,
|
||||
imageContentReader: ImageContentReader,
|
||||
messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter },
|
||||
) {
|
||||
this.install { (httpClient, _, installedServices) ->
|
||||
SERVICE_KEY to DefaultMessageService(httpClient, localEchoStore, backgroundScheduler, messageEncrypter.create(installedServices))
|
||||
SERVICE_KEY to DefaultMessageService(httpClient, localEchoStore, backgroundScheduler, messageEncrypter.create(installedServices), imageContentReader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,16 +20,20 @@ internal class DefaultMessageService(
|
||||
private val localEchoStore: LocalEchoStore,
|
||||
private val backgroundScheduler: BackgroundScheduler,
|
||||
messageEncrypter: MessageEncrypter,
|
||||
imageContentReader: ImageContentReader,
|
||||
) : MessageService, MatrixTaskRunner {
|
||||
|
||||
private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter)
|
||||
private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, imageContentReader)
|
||||
private val sendEventMessageUseCase = SendEventMessageUseCase(httpClient)
|
||||
|
||||
override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE
|
||||
override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE || task.type == MATRIX_IMAGE_MESSAGE_TASK_TYPE
|
||||
|
||||
override suspend fun run(task: MatrixTaskRunner.MatrixTask): MatrixTaskRunner.TaskResult {
|
||||
require(task.type == MATRIX_MESSAGE_TASK_TYPE)
|
||||
val message = Json.decodeFromString(MessageService.Message.TextMessage.serializer(), task.jsonPayload)
|
||||
val message = when(task.type) {
|
||||
MATRIX_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.TextMessage.serializer(), task.jsonPayload)
|
||||
MATRIX_IMAGE_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.ImageMessage.serializer(), task.jsonPayload)
|
||||
else -> throw IllegalStateException("Unhandled task type: ${task.type}")
|
||||
}
|
||||
return try {
|
||||
sendMessage(message)
|
||||
MatrixTaskRunner.TaskResult.Success
|
||||
@ -70,6 +74,7 @@ internal class DefaultMessageService(
|
||||
Json.encodeToString(MessageService.Message.TextMessage.serializer(), this)
|
||||
)
|
||||
}
|
||||
|
||||
is MessageService.Message.ImageMessage -> BackgroundScheduler.Task(
|
||||
type = MATRIX_IMAGE_MESSAGE_TASK_TYPE,
|
||||
Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this)
|
||||
|
@ -9,6 +9,7 @@ import app.dapk.st.matrix.message.MessageService
|
||||
internal class SendMessageUseCase(
|
||||
private val httpClient: MatrixHttpClient,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val imageContentReader: ImageContentReader,
|
||||
) {
|
||||
|
||||
suspend fun sendMessage(message: MessageService.Message): EventId {
|
||||
@ -23,6 +24,7 @@ internal class SendMessageUseCase(
|
||||
content = messageEncrypter.encrypt(message),
|
||||
)
|
||||
}
|
||||
|
||||
false -> {
|
||||
sendRequest(
|
||||
roomId = message.roomId,
|
||||
@ -34,37 +36,68 @@ internal class SendMessageUseCase(
|
||||
}
|
||||
httpClient.execute(request).eventId
|
||||
}
|
||||
is MessageService.Message.ImageMessage -> {
|
||||
// upload image, then send message
|
||||
// POST /_matrix/media/v3/upload
|
||||
// message.content.uri
|
||||
|
||||
/**
|
||||
* {
|
||||
"content": {
|
||||
"body": "filename.jpg",
|
||||
"info": {
|
||||
"h": 398,
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 31037,
|
||||
"w": 394
|
||||
},
|
||||
"msgtype": "m.image",
|
||||
"url": "mxc://example.org/JWEIFJgwEIhweiWJE"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
*/
|
||||
TODO()
|
||||
is MessageService.Message.ImageMessage -> {
|
||||
println("Sending message")
|
||||
val imageContent = imageContentReader.read(message.content.uri)
|
||||
|
||||
println("content: ${imageContent.size}")
|
||||
|
||||
val uri = httpClient.execute(uploadRequest(imageContent.content, imageContent.fileName, "image/png")).contentUri
|
||||
println("Got uri $uri")
|
||||
|
||||
val request = sendRequest(
|
||||
roomId = message.roomId,
|
||||
eventType = EventType.ROOM_MESSAGE,
|
||||
txId = message.localId,
|
||||
content = MessageService.Message.Content.ImageContent(
|
||||
url = uri,
|
||||
filename = "foobar.png",
|
||||
MessageService.Message.Content.ImageContent.Info(
|
||||
height = imageContent.height,
|
||||
width = imageContent.width,
|
||||
size = imageContent.size
|
||||
)
|
||||
),
|
||||
)
|
||||
httpClient.execute(request).eventId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface ImageContentReader {
|
||||
fun read(uri: String): ImageContent
|
||||
|
||||
data class ImageContent(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val size: Long,
|
||||
val fileName: String,
|
||||
val content: ByteArray
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ImageContent
|
||||
|
||||
if (height != other.height) return false
|
||||
if (width != other.width) return false
|
||||
if (size != other.size) return false
|
||||
if (!content.contentEquals(other.content)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = height
|
||||
result = 31 * result + width
|
||||
result = 31 * result + size.hashCode()
|
||||
result = 31 * result + content.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
@ -6,9 +6,12 @@ import app.dapk.st.matrix.http.MatrixHttpClient
|
||||
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest
|
||||
import app.dapk.st.matrix.http.jsonBody
|
||||
import app.dapk.st.matrix.message.ApiSendResponse
|
||||
import app.dapk.st.matrix.message.ApiUploadResponse
|
||||
import app.dapk.st.matrix.message.MessageEncrypter
|
||||
import app.dapk.st.matrix.message.MessageService.EventMessage
|
||||
import app.dapk.st.matrix.message.MessageService.Message
|
||||
import io.ktor.content.*
|
||||
import io.ktor.http.*
|
||||
import java.util.*
|
||||
|
||||
internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: Message.Content) = httpRequest<ApiSendResponse>(
|
||||
@ -17,6 +20,7 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, con
|
||||
body = when (content) {
|
||||
is Message.Content.TextContent -> jsonBody(Message.Content.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
|
||||
is Message.Content.ImageContent -> jsonBody(Message.Content.ImageContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
|
||||
is Message.Content.ApiImageContent -> throw IllegalArgumentException()
|
||||
}
|
||||
)
|
||||
|
||||
@ -34,4 +38,12 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMes
|
||||
}
|
||||
)
|
||||
|
||||
internal fun uploadRequest(body: ByteArray, filename: String, contentType: String) = httpRequest<ApiUploadResponse>(
|
||||
path = "_matrix/media/r0/upload/?filename=$filename",
|
||||
headers = listOf("Content-Type" to contentType),
|
||||
method = MatrixHttpClient.Method.POST,
|
||||
body = ByteArrayContent(body, ContentType.parse(contentType)),
|
||||
)
|
||||
|
||||
|
||||
fun txId() = "local.${UUID.randomUUID()}"
|
@ -44,6 +44,7 @@ internal class RoomEventFactory(
|
||||
|
||||
private fun ApiTimelineEvent.TimelineMessage.readImageMeta(userCredentials: UserCredentials): RoomEvent.Image.ImageMeta {
|
||||
val content = this.content as ApiTimelineEvent.TimelineMessage.Content.Image
|
||||
println(content)
|
||||
return RoomEvent.Image.ImageMeta(
|
||||
content.info?.width,
|
||||
content.info?.height,
|
||||
|
@ -22,6 +22,7 @@ import test.MatrixTestScope
|
||||
import test.TestMatrix
|
||||
import test.flowTest
|
||||
import test.restoreLoginAndInitialSync
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
private const val HTTPS_TEST_SERVER_URL = "https://localhost:8080/"
|
||||
@ -73,6 +74,14 @@ class SmokeTest {
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
fun `can send and receive clear image messages`() = testAfterInitialSync { alice, bob ->
|
||||
val testImage = loadResourceFile("test-image.png")
|
||||
alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = false)
|
||||
bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
fun `can request and verify devices`() = testAfterInitialSync { alice, bob ->
|
||||
alice.client.cryptoService().verificationAction(Verification.Action.Request(bob.userId(), bob.deviceId()))
|
||||
alice.client.cryptoService().verificationState().automaticVerification(alice).expectAsync { it == Verification.State.Done }
|
||||
@ -85,7 +94,7 @@ class SmokeTest {
|
||||
fun `can import E2E room keys file`() = runTest {
|
||||
val ignoredUser = TestUser("ignored", RoomMember(UserId("ignored"), null, null), "ignored")
|
||||
val cryptoService = TestMatrix(ignoredUser, includeLogging = true).client.cryptoService()
|
||||
val stream = Thread.currentThread().contextClassLoader.getResourceAsStream("element-keys.txt")!!
|
||||
val stream = loadResourceStream("element-keys.txt")
|
||||
|
||||
val result = with(cryptoService) {
|
||||
stream.importRoomKeys(password = "aaaaaa")
|
||||
@ -182,4 +191,7 @@ private fun Flow<Verification.State>.automaticVerification(testMatrix: TestMatri
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadResourceStream(name: String) = Thread.currentThread().contextClassLoader.getResourceAsStream(name)!!
|
||||
private fun loadResourceFile(name: String) = Paths.get(Thread.currentThread().contextClassLoader.getResource(name)!!.toURI()).toFile()
|
||||
|
@ -5,17 +5,31 @@ package test
|
||||
import TestMessage
|
||||
import TestUser
|
||||
import app.dapk.st.core.extensions.ifNull
|
||||
import app.dapk.st.matrix.common.MxUrl
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.convertMxUrToUrl
|
||||
import app.dapk.st.matrix.http.MatrixHttpClient
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.syncService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.util.cio.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
fun flowTest(block: suspend MatrixTestScope.() -> Unit) {
|
||||
@ -131,6 +145,20 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
.assert(message)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.expectImageMessage(roomId: RoomId, image: File, author: RoomMember) {
|
||||
println("expecting ${image.absolutePath} from ${author.displayName}")
|
||||
this.client.syncService().room(roomId)
|
||||
.map {
|
||||
it.events.filterIsInstance<RoomEvent.Image>().map {
|
||||
println("found: ${it.imageMeta.url}")
|
||||
val output = File(image.parentFile.absolutePath, "output.png")
|
||||
HttpClient().request(it.imageMeta.url).bodyAsChannel().copyAndClose(output.writeChannel())
|
||||
output.readBytes().md5Hash() to it.author
|
||||
}.firstOrNull()
|
||||
}
|
||||
.assert(image.readBytes().md5Hash() to author)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.sendTextMessage(roomId: RoomId, content: String, isEncrypted: Boolean) {
|
||||
println("sending $content")
|
||||
this.client.messageService().scheduleMessage(
|
||||
@ -144,6 +172,21 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.sendImageMessage(roomId: RoomId, file: File, isEncrypted: Boolean) {
|
||||
println("sending ${file.name}")
|
||||
this.client.messageService().scheduleMessage(
|
||||
MessageService.Message.ImageMessage(
|
||||
content = MessageService.Message.Content.ApiImageContent(
|
||||
uri = file.absolutePath,
|
||||
),
|
||||
roomId = roomId,
|
||||
sendEncrypted = isEncrypted,
|
||||
localId = "local.${UUID.randomUUID()}",
|
||||
timestampUtc = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.loginWithInitialSync() {
|
||||
this.restoreLogin()
|
||||
client.syncService().startSyncing().collectAsync(testScope) {
|
||||
@ -162,5 +205,10 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
suspend fun release() {
|
||||
inProgressInstances.forEach { it.release() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.md5Hash(): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val bigInt = BigInteger(1, md.digest(this))
|
||||
return String.format("%032x", bigInt)
|
||||
}
|
@ -22,6 +22,7 @@ import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||
import app.dapk.st.matrix.message.MessageEncrypter
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.installMessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.push.installPushService
|
||||
import app.dapk.st.matrix.room.RoomMessenger
|
||||
@ -41,7 +42,9 @@ import test.impl.InMemoryDatabase
|
||||
import test.impl.InMemoryPreferences
|
||||
import test.impl.InstantScheduler
|
||||
import test.impl.PrintingErrorTracking
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
class TestMatrix(
|
||||
private val user: TestUser,
|
||||
@ -114,7 +117,7 @@ class TestMatrix(
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
|
||||
installMessageService(storeModule.localEchoStore, InstantScheduler(it)) { serviceProvider ->
|
||||
installMessageService(storeModule.localEchoStore, InstantScheduler(it), JavaImageContentReader()) { serviceProvider ->
|
||||
MessageEncrypter { message ->
|
||||
val result = serviceProvider.cryptoService().encrypt(
|
||||
roomId = when (message) {
|
||||
@ -133,6 +136,7 @@ class TestMatrix(
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageService.Message.ImageMessage -> TODO()
|
||||
}
|
||||
)
|
||||
@ -200,12 +204,14 @@ class TestMatrix(
|
||||
apiEvent.content.methods,
|
||||
apiEvent.content.timestampPosix,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.methods,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
@ -216,6 +222,7 @@ class TestMatrix(
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationAccept -> Verification.Event.Accepted(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
@ -226,12 +233,14 @@ class TestMatrix(
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.key
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
@ -288,6 +297,7 @@ class TestMatrix(
|
||||
|
||||
suspend fun deviceId() = storeModule.credentialsStore().credentials()!!.deviceId
|
||||
suspend fun userId() = storeModule.credentialsStore().credentials()!!.userId
|
||||
suspend fun credentials() = storeModule.credentialsStore().credentials()!!
|
||||
|
||||
suspend fun release() {
|
||||
coroutineDispatchers.global.waitForCancel()
|
||||
@ -316,4 +326,21 @@ class JavaBase64 : Base64 {
|
||||
override fun decode(input: String): ByteArray {
|
||||
return java.util.Base64.getDecoder().decode(input)
|
||||
}
|
||||
}
|
||||
|
||||
class JavaImageContentReader : ImageContentReader {
|
||||
|
||||
override fun read(uri: String): ImageContentReader.ImageContent {
|
||||
val file = File(uri)
|
||||
val size = file.length()
|
||||
val image = ImageIO.read(file)
|
||||
return ImageContentReader.ImageContent(
|
||||
height = image.height,
|
||||
width = image.width,
|
||||
size = size,
|
||||
fileName = file.name,
|
||||
content = file.readBytes()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
BIN
test-harness/src/test/resources/test-image.png
Normal file
BIN
test-harness/src/test/resources/test-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
Loading…
x
Reference in New Issue
Block a user