All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.net.folivo.trixnity.clientserverapi.client.UserApiClient.kt Maven / Gradle / Ivy

There is a newer version: 4.11.2
Show newest version
package net.folivo.trixnity.clientserverapi.client

import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.reflect.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import net.folivo.trixnity.clientserverapi.model.users.*
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.GlobalAccountDataEventContent
import net.folivo.trixnity.core.model.events.ToDeviceEventContent
import net.folivo.trixnity.core.model.events.m.Presence
import net.folivo.trixnity.core.model.events.m.PresenceEventContent
import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings
import net.folivo.trixnity.core.serialization.events.contentType
import net.folivo.trixnity.utils.nextString
import kotlin.random.Random

interface UserApiClient {
    val contentMappings: EventContentSerializerMappings

    /**
     * @see [GetDisplayName]
     */
    suspend fun getDisplayName(
        userId: UserId,
    ): Result

    /**
     * @see [SetDisplayName]
     */
    suspend fun setDisplayName(
        userId: UserId,
        displayName: String? = null,
        asUserId: UserId? = null
    ): Result

    /**
     * @see [GetAvatarUrl]
     */
    suspend fun getAvatarUrl(
        userId: UserId,
    ): Result

    /**
     * @see [SetAvatarUrl]
     */
    suspend fun setAvatarUrl(
        userId: UserId,
        avatarUrl: String?,
        asUserId: UserId? = null,
    ): Result

    /**
     * @see [GetProfile]
     */
    suspend fun getProfile(
        userId: UserId,
    ): Result

    /**
     * @see [GetPresence]
     */
    suspend fun getPresence(
        userId: UserId,
        asUserId: UserId? = null
    ): Result

    /**
     * @see [SetPresence]
     */
    suspend fun setPresence(
        userId: UserId,
        presence: Presence,
        statusMessage: String? = null,
        asUserId: UserId? = null
    ): Result

    /**
     * @see [SendToDevice]
     */
    suspend fun  sendToDeviceUnsafe(
        events: Map>,
        transactionId: String = Random.nextString(22),
        asUserId: UserId? = null
    ): Result

    /**
     * @see [SendToDevice]
     */
    suspend fun sendToDeviceUnsafe(
        type: String,
        events: Map>,
        transactionId: String = Random.nextString(22),
        asUserId: UserId? = null
    ): Result

    /**
     * This splits [events] into multiple requests, when they have a different type
     * (for example a mix of encrypted and unencrypted events).
     *
     * @see [SendToDevice]
     */
    suspend fun sendToDevice(
        events: Map>,
        asUserId: UserId? = null
    ): Result

    /**
     * @see [GetFilter]
     */
    suspend fun getFilter(
        userId: UserId,
        filterId: String,
        asUserId: UserId? = null
    ): Result

    /**
     * @see [SetFilter]
     */
    suspend fun setFilter(
        userId: UserId,
        filters: Filters,
        asUserId: UserId? = null
    ): Result

    /**
     * @see [GetGlobalAccountData]
     */
    suspend fun getAccountData(
        type: String,
        userId: UserId,
        key: String = "",
        asUserId: UserId? = null
    ): Result

    /**
     * @see [SetGlobalAccountData]
     */
    suspend fun setAccountData(
        content: GlobalAccountDataEventContent,
        userId: UserId,
        key: String = "",
        asUserId: UserId? = null
    ): Result

    /**
     *  @see [SearchUsers]
     */
    suspend fun searchUsers(
        searchTerm: String,
        acceptLanguage: String,
        limit: Long? = 10,
        asUserId: UserId? = null,
    ): Result
}

class UserApiClientImpl(
    private val httpClient: MatrixClientServerApiHttpClient,
    override val contentMappings: EventContentSerializerMappings
) : UserApiClient {

    override suspend fun getDisplayName(
        userId: UserId,
    ): Result =
        httpClient.request(GetDisplayName(userId)).mapCatching { it.displayName }

    override suspend fun setDisplayName(
        userId: UserId,
        displayName: String?,
        asUserId: UserId?
    ): Result =
        httpClient.request(SetDisplayName(userId, asUserId), SetDisplayName.Request(displayName))

    override suspend fun getAvatarUrl(
        userId: UserId,
    ): Result =
        httpClient.request(GetAvatarUrl(userId)).mapCatching { it.avatarUrl }

    override suspend fun setAvatarUrl(
        userId: UserId,
        avatarUrl: String?,
        asUserId: UserId?,
    ): Result =
        httpClient.request(SetAvatarUrl(userId, asUserId), SetAvatarUrl.Request(avatarUrl))

    override suspend fun getProfile(
        userId: UserId,
    ): Result =
        httpClient.request(GetProfile(userId))

    override suspend fun getPresence(
        userId: UserId,
        asUserId: UserId?
    ): Result =
        httpClient.request(GetPresence(userId, asUserId))

    override suspend fun setPresence(
        userId: UserId,
        presence: Presence,
        statusMessage: String?,
        asUserId: UserId?
    ): Result =
        httpClient.request(SetPresence(userId, asUserId), SetPresence.Request(presence, statusMessage))


    override suspend fun  sendToDeviceUnsafe(
        events: Map>,
        transactionId: String,
        asUserId: UserId?
    ): Result {
        val firstEventForType = events.entries.firstOrNull()?.value?.entries?.firstOrNull()?.value
        requireNotNull(firstEventForType) { "you need to send at least on event" }
        require(events.flatMap { it.value.values }
            .all { it.instanceOf(firstEventForType::class) }) { "all events must be of the same type" }
        val type = contentMappings.toDevice.contentType(firstEventForType)
        return sendToDeviceUnsafe(type, events, transactionId, asUserId)
    }

    override suspend fun sendToDeviceUnsafe(
        type: String,
        events: Map>,
        transactionId: String,
        asUserId: UserId?
    ): Result =
        httpClient.request(SendToDevice(type, transactionId, asUserId), SendToDevice.Request(events))

    override suspend fun sendToDevice(
        events: Map>,
        asUserId: UserId?,
    ): Result = runCatching {
        data class FlatEntry(
            val userId: UserId,
            val deviceId: String,
            val event: ToDeviceEventContent,
        )

        val flatEvents = events.flatMap { (userId, deviceEvents) ->
            deviceEvents.map { (deviceId, deviceEvent) ->
                FlatEntry(userId, deviceId, deviceEvent)
            }
        }
        if (flatEvents.isNotEmpty()) {
            val eventsByType = flatEvents
                .groupBy { it.event::class }
                .mapValues { (_, flatEntryByUserId) ->
                    flatEntryByUserId.groupBy { it.userId }
                        .mapValues { (_, flatEntryByDeviceId) ->
                            flatEntryByDeviceId.associate { it.deviceId to it.event }
                        }
                }
            coroutineScope {
                eventsByType.values.forEach {
                    launch {
                        sendToDeviceUnsafe(it, asUserId = asUserId).getOrThrow()
                    }
                }
            }
        }
    }

    override suspend fun getFilter(
        userId: UserId,
        filterId: String,
        asUserId: UserId?
    ): Result =
        httpClient.request(GetFilter(userId, filterId, asUserId))

    override suspend fun setFilter(
        userId: UserId,
        filters: Filters,
        asUserId: UserId?
    ): Result =
        httpClient.request(SetFilter(userId, asUserId), filters).mapCatching { it.filterId }

    override suspend fun getAccountData(
        type: String,
        userId: UserId,
        key: String,
        asUserId: UserId?
    ): Result {
        val actualType = if (key.isEmpty()) type else type + key
        return httpClient.request(GetGlobalAccountData(userId, actualType, asUserId))
    }

    override suspend fun setAccountData(
        content: GlobalAccountDataEventContent,
        userId: UserId,
        key: String,
        asUserId: UserId?
    ): Result {
        val eventType = contentMappings.globalAccountData.contentType(content)
            .let { type -> if (key.isEmpty()) type else type + key }

        return httpClient.request(SetGlobalAccountData(userId, eventType, asUserId), content)
    }

    override suspend fun searchUsers(
        searchTerm: String,
        acceptLanguage: String,
        limit: Long?,
        asUserId: UserId?,
    ): Result =
        httpClient.request(SearchUsers(asUserId), SearchUsers.Request(searchTerm, limit)) {
            header(HttpHeaders.AcceptLanguage, acceptLanguage)
        }
}

/**
 * @see [GetGlobalAccountData]
 */
suspend inline fun  UserApiClient.getAccountData(
    userId: UserId,
    key: String = "",
    asUserId: UserId? = null
): Result {
    val type = contentMappings.globalAccountData.contentType(C::class)
    @Suppress("UNCHECKED_CAST")
    return getAccountData(type, userId, key, asUserId) as Result
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy