commonMain.net.folivo.trixnity.clientserverapi.client.UserApiClient.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trixnity-clientserverapi-client Show documentation
Show all versions of trixnity-clientserverapi-client Show documentation
Multiplatform Kotlin SDK for matrix-protocol
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