Usecase.updateRoomNameInfo()

This commit is contained in:
2022-04-18 23:04:30 +02:00
parent c0a149cadd
commit 89b6c90b65
10 changed files with 214 additions and 144 deletions

View File

@@ -1,22 +0,0 @@
package eu.fosil.okupamicoche.matrix
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.json.Json
import net.folivo.trixnity.clientserverapi.client.*
import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings
interface IMatrixClientServerApiClient {
val json: Json
val eventContentSerializerMappings: EventContentSerializerMappings
val accessToken: MutableStateFlow<String?>
val httpClient: MatrixClientServerApiHttpClient
val authentication: IAuthenticationApiClient
val server: IServerApiClient
val users: IUsersApiClient
val rooms: IRoomsApiClient
val sync: ISyncApiClient
val keys: IKeysApiClient
val media: IMediaApiClient
val devices: IDevicesApiClient
val push: IPushApiClient
}

View File

@@ -2,17 +2,13 @@ package eu.fosil.okupamicoche.matrix
import eu.fosil.okupamicoche.config.Config
import eu.fosil.okupamicoche.matrix.event.TravelEventContentSerializerMappings
import eu.fosil.okupamicoche.matrix.event.state.MembershipStateEventContent
import eu.fosil.okupamicoche.matrix.event.state.TravelCreatedStateEventContent
import io.ktor.http.*
import mu.KotlinLogging
import net.folivo.trixnity.clientserverapi.client.MatrixClientServerApiClient
import net.folivo.trixnity.clientserverapi.client.getStateEvent
import net.folivo.trixnity.clientserverapi.model.rooms.DirectoryVisibility
import net.folivo.trixnity.core.model.RoomAliasId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.m.room.Membership
import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent
import net.folivo.trixnity.core.serialization.createEventContentSerializerMappings
import net.folivo.trixnity.core.serialization.createMatrixJson
@@ -41,31 +37,12 @@ object MatrixApiClient {
mainRoomId = createMainRoomIfNeeded(config)
}
suspend fun getDisplayName(userId: UserId) =
client.users.getDisplayName(userId).getOrNull() ?: userId.localpart
suspend fun isTravel(roomId: RoomId) =
client.rooms.getStateEvent<TravelCreatedStateEventContent>(
roomId
).getOrNull() !== null
suspend fun isDriver(roomId: RoomId, userId: UserId) =
client.rooms.getStateEvent<TravelCreatedStateEventContent>(
roomId
).getOrNull()?.driver == userId.full
suspend fun getTravelMembership(roomId: RoomId, userId: UserId) =
client.rooms.getStateEvent<MembershipStateEventContent>(
roomId,
userId.full.substring(1) // Synapse throws 403 if stateKey starts with '@'
).getOrNull()?.membership
suspend fun sendErrorTextMessage(
roomId: RoomId,
body: String,
formattedBody: String? = null
) {
val formattedBodyWithWarningIcon = "⚠️" + (formattedBody ?: body)
val formattedBodyWithWarningIcon = "⚠️ " + (formattedBody ?: body)
client.rooms.sendMessageEvent(
roomId,
RoomMessageEventContent.TextMessageEventContent(
@@ -76,22 +53,10 @@ object MatrixApiClient {
)
}
suspend fun getAvailablePlaces(roomId: RoomId): Int? {
// Get travel total places (not counting driver)
val roomState = client.rooms.getStateEvent<TravelCreatedStateEventContent>(roomId).getOrNull() ?: return null
var availablePlaces = roomState.places
suspend fun isRoomAliasAvailable(roomAliasId: RoomAliasId): Boolean {
val roomId = MatrixApiClient.client.rooms.getRoomAlias(roomAliasId).getOrNull()
val states = client.rooms.getState(roomId).getOrNull()
logger.info { "states size=${states?.size}" }
// Remove one place on each Join
client.rooms.getState(roomId).getOrNull()?.filter {
val content = it.content
(content is MembershipStateEventContent) && (content.membership == Membership.JOIN.value)
}?.forEach { _ -> availablePlaces-- }
return availablePlaces
return roomId == null
}
private suspend fun createMainRoomIfNeeded(config: Config): RoomId? {

View File

@@ -21,14 +21,17 @@ data class TravelCreatedStateEventContent(
@SerialName("places")
val places: Int,
@SerialName("description")
val description: String
val description: String,
@SerialName("duplicateNum")
val duplicateNum: Int?
) : StateEventContent {
constructor(travelOptions: TravelOptions, driver: UserId) : this(
constructor(travelOptions: TravelOptions, driver: UserId, duplicateNum: Int? = null) : this(
driver.full,
travelOptions.from,
travelOptions.to,
travelOptions.time,
travelOptions.places,
travelOptions.description
travelOptions.description,
duplicateNum
)
}

View File

@@ -2,11 +2,16 @@ package eu.fosil.okupamicoche.model
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class Travel(
var room: RoomId,
var driver: UserId,
var options: TravelOptions
var options: TravelOptions,
var travelers: List<UserId> = listOf(),
var duplicateNum: Int? = null
)
class TravelOptions(
@@ -15,4 +20,23 @@ class TravelOptions(
var time: Long,
var places: Int,
var description: String
)
) {
fun getRoomAliasPrefix(): String {
val instant = Instant.ofEpochMilli(time)
val date = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault()).format(instant)
val time =
DateTimeFormatter.ofPattern("H:mm").withZone(ZoneId.systemDefault()).format(instant).replace(':', 'H')
return "#viaje_${from}-${to}_${date}_${time}"
}
fun getRoomName(duplicateNum: Int?, travelers: List<UserId>? = null): String {
val instant = Instant.ofEpochMilli(time)
val date = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault()).format(instant)
val time = DateTimeFormatter.ofPattern("H:mm").withZone(ZoneId.systemDefault()).format(instant)
val attemptSuffix = if ((duplicateNum ?: 0) > 0) " ($duplicateNum)" else ""
val availablePlaces = places - (travelers?.size ?: 0)
return "Travel ${from}-${to} $date $time | $availablePlaces places available" + attemptSuffix
}
}

View File

@@ -0,0 +1,68 @@
package eu.fosil.okupamicoche.model
import eu.fosil.okupamicoche.matrix.MatrixApiClient
import eu.fosil.okupamicoche.matrix.event.state.MembershipStateEventContent
import eu.fosil.okupamicoche.matrix.event.state.TravelCreatedStateEventContent
import net.folivo.trixnity.clientserverapi.client.getStateEvent
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.m.room.Membership
object TravelRepository {
suspend fun getDisplayName(userId: UserId) =
MatrixApiClient.client.users.getDisplayName(userId).getOrNull() ?: userId.localpart
suspend fun isTravel(roomId: RoomId) =
MatrixApiClient.client.rooms.getStateEvent<TravelCreatedStateEventContent>(
roomId
).getOrNull() !== null
suspend fun getTravel(roomId: RoomId): Travel? {
val travelCreatedstateEvent = MatrixApiClient.client.rooms.getStateEvent<TravelCreatedStateEventContent>(
roomId
).getOrNull() ?: return null
return Travel(
room = roomId,
driver = UserId(travelCreatedstateEvent.driver),
options = TravelOptions(
from = travelCreatedstateEvent.from,
to = travelCreatedstateEvent.to,
time = travelCreatedstateEvent.time,
places = travelCreatedstateEvent.places,
description = travelCreatedstateEvent.description
),
travelers = getTravelers(roomId) ?: listOf(),
duplicateNum = travelCreatedstateEvent.duplicateNum
)
}
suspend fun isDriver(roomId: RoomId, userId: UserId) =
MatrixApiClient.client.rooms.getStateEvent<TravelCreatedStateEventContent>(
roomId
).getOrNull()?.driver == userId.full
suspend fun getTravelMembership(roomId: RoomId, userId: UserId) =
MatrixApiClient.client.rooms.getStateEvent<MembershipStateEventContent>(
roomId,
userId.full.substring(1) // Synapse throws 403 if stateKey starts with '@'
).getOrNull()?.membership
suspend fun getTravelers(roomId: RoomId) = MatrixApiClient.client.rooms.getState(roomId).getOrNull()?.filter {
val content = it.content
(content is MembershipStateEventContent) && (content.membership == Membership.JOIN.value)
}?.map { UserId("@${it.stateKey}") }
suspend fun getAvailablePlaces(roomId: RoomId): Int? {
// Get travel total places (not counting driver)
val roomState = MatrixApiClient.client.rooms.getStateEvent<TravelCreatedStateEventContent>(roomId).getOrNull()
?: return null
// Get travelers count
val travelersCount = getTravelers(roomId)?.size ?: 0
return roomState.places - travelersCount
}
}

View File

@@ -1,6 +1,5 @@
package eu.fosil.okupamicoche.usecase.travel
import eu.fosil.okupamicoche.config.ConfigReader
import eu.fosil.okupamicoche.db.TravelEntity
import eu.fosil.okupamicoche.matrix.MatrixApiClient
import eu.fosil.okupamicoche.matrix.db
@@ -9,10 +8,9 @@ import eu.fosil.okupamicoche.matrix.event.state.TRAVEL_CREATED_STATE_EVENT_TYPE
import eu.fosil.okupamicoche.matrix.event.state.TravelCreatedStateEventContent
import eu.fosil.okupamicoche.model.Travel
import eu.fosil.okupamicoche.model.TravelOptions
import eu.fosil.okupamicoche.model.TravelRepository
import eu.fosil.okupamicoche.usecase.Usecase
import mu.KotlinLogging
import net.folivo.trixnity.clientserverapi.model.rooms.DirectoryVisibility
import net.folivo.trixnity.core.model.RoomAliasId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.Event
@@ -23,13 +21,6 @@ import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
private val logger = KotlinLogging.logger {}
data class RoomAliasAndName(
val aliasId: RoomAliasId,
val name: String
)
suspend fun Usecase.createTravel(
travelOptions: TravelOptions,
driver: UserId,
@@ -52,9 +43,13 @@ suspend fun Usecase.createTravel(
private suspend fun createRoom(travelOptions: TravelOptions, driver: UserId): RoomId {
val appServiceUserId = requireNotNull(MatrixApiClient.appServiceUserId)
val newRoomAliasAndName = getRoomAliasAndName(travelOptions)
val newRoomNameInfo = Usecase.getRoomNameInfoFromTravelOptions(travelOptions)
val initialState = Event.InitialStateEvent(
TravelCreatedStateEventContent(travelOptions, driver),
TravelCreatedStateEventContent(
travelOptions,
driver,
newRoomNameInfo.duplicateNum
),
""
)
val powerLevels = PowerLevelsEventContent(
@@ -67,8 +62,8 @@ private suspend fun createRoom(travelOptions: TravelOptions, driver: UserId): Ro
return MatrixApiClient.client.rooms.createRoom(
visibility = DirectoryVisibility.PUBLIC,
roomAliasId = newRoomAliasAndName.aliasId,
name = newRoomAliasAndName.name,
roomAliasId = newRoomNameInfo.aliasId,
name = newRoomNameInfo.name,
topic = travelOptions.description,
invite = setOf(driver),
roomVersion = "9",
@@ -86,56 +81,6 @@ private suspend fun createRoom(travelOptions: TravelOptions, driver: UserId): Ro
// )
}
private suspend fun getRoomAliasAndName(travelOptions: TravelOptions): RoomAliasAndName {
val roomAliasPrefix = getRoomAliasPrefix(travelOptions)
logger.info { "roomAliasPrefix=$roomAliasPrefix" }
var attempt = 0
// Look for available room alias
while (!isRoomAliasAvailable(getRoomAliasAttempt(roomAliasPrefix, attempt)))
attempt++
logger.info { "getRoomAlias=${getRoomAliasAttempt(roomAliasPrefix, attempt)}" }
return RoomAliasAndName(
getRoomAliasAttempt(roomAliasPrefix, attempt),
getRoomName(travelOptions, attempt)
)
}
private fun getRoomAliasPrefix(travelOptions: TravelOptions): String {
val instant = Instant.ofEpochMilli(travelOptions.time)
val date = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault()).format(instant)
val time = DateTimeFormatter.ofPattern("H:mm").withZone(ZoneId.systemDefault()).format(instant).replace(':', 'H')
return travelOptions.run {
"#viaje_${from}-${to}_${date}_${time}"
}
}
private fun getRoomAliasAttempt(roomAliasPrefix: String, attempt: Int): RoomAliasId {
val config = requireNotNull(ConfigReader.config)
val attemptPart = if (attempt <= 0) "" else "_$attempt"
return RoomAliasId("${roomAliasPrefix}$attemptPart:${config.homeserver.host}")
}
private suspend fun isRoomAliasAvailable(roomAliasId: RoomAliasId): Boolean {
val roomId = MatrixApiClient.client.rooms.getRoomAlias(roomAliasId).getOrNull()
logger.info { "$roomAliasId roomId=$roomId" }
return roomId == null
}
private fun getRoomName(travelOptions: TravelOptions, attempt: Int): String {
val instant = Instant.ofEpochMilli(travelOptions.time)
val date = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault()).format(instant)
val time = DateTimeFormatter.ofPattern("H:mm").withZone(ZoneId.systemDefault()).format(instant)
val attemptSuffix = if (attempt > 0) " ($attempt)" else ""
return travelOptions.run {
"Travel ${from}-${to} $date $time | $places places available" + attemptSuffix
}
}
private suspend fun sendCreateMessageEvents(
travel: Travel,
createdFromRoomId: RoomId
@@ -147,12 +92,19 @@ private suspend fun sendCreateMessageEvents(
val time = DateTimeFormatter.ofPattern("H:mm").withZone(ZoneId.systemDefault()).format(instant)
// Send text message to main room
val displayName = MatrixApiClient.getDisplayName(travel.driver)
val messageBody = "$displayName created a new travel! ${travel.options.from}-${travel.options.to}" +
val displayName = TravelRepository.getDisplayName(travel.driver)
val body = "$displayName created a new travel! ${travel.options.from}-${travel.options.to}" +
" on $date $time with ${travel.options.places} free places."
val formattedBody =
"\uD83D\uDE97 $displayName created a new travel!<br><br>${travel.options.from}-${travel.options.to}" +
" on $date $time with ${travel.options.places} free places."
MatrixApiClient.client.rooms.sendMessageEvent(
mainRoomId,
RoomMessageEventContent.TextMessageEventContent(messageBody)
RoomMessageEventContent.TextMessageEventContent(
body = body,
format = "org.matrix.custom.html",
formattedBody = formattedBody
)
)
// Send new travel message event to main room
@@ -165,7 +117,11 @@ private suspend fun sendCreateMessageEvents(
if (createdFromRoomId != mainRoomId) {
MatrixApiClient.client.rooms.sendMessageEvent(
createdFromRoomId,
RoomMessageEventContent.TextMessageEventContent(messageBody)
RoomMessageEventContent.TextMessageEventContent(
body = "Travel created!",
format = "org.matrix.custom.html",
formattedBody = "\uD83D\uDE97 Travel created!"
)
)
}
}

View File

@@ -0,0 +1,55 @@
package eu.fosil.okupamicoche.usecase.travel
import eu.fosil.okupamicoche.config.ConfigReader
import eu.fosil.okupamicoche.matrix.MatrixApiClient.isRoomAliasAvailable
import eu.fosil.okupamicoche.model.TravelOptions
import eu.fosil.okupamicoche.model.TravelRepository
import eu.fosil.okupamicoche.usecase.Usecase
import net.folivo.trixnity.core.model.RoomAliasId
import net.folivo.trixnity.core.model.RoomId
data class RoomNameInfo(
val aliasId: RoomAliasId,
val name: String,
val duplicateNum: Int?
)
suspend fun Usecase.getRoomNameInfoFromTravelOptions(travelOptions: TravelOptions): RoomNameInfo {
val roomAliasPrefix = travelOptions.getRoomAliasPrefix()
val duplicateNum = calculateDuplicateNum(roomAliasPrefix)
return RoomNameInfo(
getRoomAlias(roomAliasPrefix, duplicateNum),
travelOptions.getRoomName(duplicateNum),
duplicateNum
)
}
suspend fun Usecase.getRoomNameFromRoomId(roomId: RoomId): RoomNameInfo? {
val travel = TravelRepository.getTravel(roomId) ?: return null
val roomAliasPrefix = travel.options.getRoomAliasPrefix()
val duplicateNum = travel.duplicateNum
val travelers = TravelRepository.getTravelers(roomId)
return RoomNameInfo(
getRoomAlias(roomAliasPrefix, duplicateNum),
travel.options.getRoomName(duplicateNum, travelers),
duplicateNum
)
}
private suspend fun calculateDuplicateNum(roomAliasPrefix: String): Int? {
var duplicateNum = 0
// Look for available room alias
while (!isRoomAliasAvailable(getRoomAlias(roomAliasPrefix, duplicateNum)))
duplicateNum++
return if (duplicateNum == 0) null else duplicateNum
}
private fun getRoomAlias(roomAliasPrefix: String, duplicateNum: Int?): RoomAliasId {
val config = requireNotNull(ConfigReader.config)
val attemptPart = if ((duplicateNum ?: 0) > 0) "_$duplicateNum" else ""
return RoomAliasId("${roomAliasPrefix}$attemptPart:${config.homeserver.host}")
}

View File

@@ -2,6 +2,7 @@ package eu.fosil.okupamicoche.usecase.travel
import eu.fosil.okupamicoche.matrix.MatrixApiClient
import eu.fosil.okupamicoche.matrix.event.state.MembershipStateEventContent
import eu.fosil.okupamicoche.model.TravelRepository
import eu.fosil.okupamicoche.usecase.Usecase
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
@@ -12,9 +13,9 @@ suspend fun Usecase.joinTravel(
roomId: RoomId,
userId: UserId
) {
val isTravel = MatrixApiClient.isTravel(roomId)
val isDriver = MatrixApiClient.isDriver(roomId, userId)
val membershipState = MatrixApiClient.getTravelMembership(roomId, userId)
val isTravel = TravelRepository.isTravel(roomId)
val isDriver = TravelRepository.isDriver(roomId, userId)
val membershipState = TravelRepository.getTravelMembership(roomId, userId)
if (!isTravel) {
MatrixApiClient.sendErrorTextMessage(roomId, "There is no travel.")
@@ -24,6 +25,7 @@ suspend fun Usecase.joinTravel(
MatrixApiClient.sendErrorTextMessage(roomId, "You are already joined.")
} else {
sendJoinedMessageEvents(roomId, userId)
Usecase.updateRoomNameInfo(roomId)
}
}
@@ -39,8 +41,8 @@ private suspend fun sendJoinedMessageEvents(
).getOrThrow()
// Send text message to travel room
val displayName = MatrixApiClient.getDisplayName(userId)
val availablePlaces = MatrixApiClient.getAvailablePlaces(roomId)
val displayName = TravelRepository.getDisplayName(userId)
val availablePlaces = TravelRepository.getAvailablePlaces(roomId)
val messageBody = "$displayName joined the travel! Places available: $availablePlaces"
MatrixApiClient.client.rooms.sendMessageEvent(
roomId,

View File

@@ -2,6 +2,7 @@ package eu.fosil.okupamicoche.usecase.travel
import eu.fosil.okupamicoche.matrix.MatrixApiClient
import eu.fosil.okupamicoche.matrix.event.state.MembershipStateEventContent
import eu.fosil.okupamicoche.model.TravelRepository
import eu.fosil.okupamicoche.usecase.Usecase
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
@@ -12,9 +13,9 @@ suspend fun Usecase.leaveTravel(
roomId: RoomId,
userId: UserId
) {
val isTravel = MatrixApiClient.isTravel(roomId)
val isDriver = MatrixApiClient.isDriver(roomId, userId)
val membershipState = MatrixApiClient.getTravelMembership(roomId, userId)
val isTravel = TravelRepository.isTravel(roomId)
val isDriver = TravelRepository.isDriver(roomId, userId)
val membershipState = TravelRepository.getTravelMembership(roomId, userId)
if (!isTravel) {
MatrixApiClient.sendErrorTextMessage(roomId, "There is no travel.")
@@ -30,6 +31,7 @@ suspend fun Usecase.leaveTravel(
MatrixApiClient.sendErrorTextMessage(roomId, "You already left.")
} else {
sendLeftMessageEvents(roomId, userId)
Usecase.updateRoomNameInfo(roomId)
}
}
@@ -45,8 +47,8 @@ private suspend fun sendLeftMessageEvents(
).getOrThrow()
// Send text message to travel room
val displayName = MatrixApiClient.getDisplayName(userId)
val availablePlaces = MatrixApiClient.getAvailablePlaces(roomId)
val displayName = TravelRepository.getDisplayName(userId)
val availablePlaces = TravelRepository.getAvailablePlaces(roomId)
val messageBody = "$displayName left the travel. Places available: $availablePlaces"
MatrixApiClient.client.rooms.sendMessageEvent(
roomId,

View File

@@ -0,0 +1,17 @@
package eu.fosil.okupamicoche.usecase.travel
import eu.fosil.okupamicoche.matrix.MatrixApiClient
import eu.fosil.okupamicoche.usecase.Usecase
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.events.m.room.NameEventContent
suspend fun Usecase.updateRoomNameInfo(roomId: RoomId) {
val roomNameInfo = Usecase.getRoomNameFromRoomId(roomId)
roomNameInfo?.let {
MatrixApiClient.client.rooms.sendStateEvent(
roomId,
NameEventContent(it.name)
)
}
}