diff --git a/backend/src/main/kotlin/com/storyteller_f/DatabaseFactory.kt b/backend/src/main/kotlin/com/storyteller_f/DatabaseFactory.kt index 932144a..6befd8b 100644 --- a/backend/src/main/kotlin/com/storyteller_f/DatabaseFactory.kt +++ b/backend/src/main/kotlin/com/storyteller_f/DatabaseFactory.kt @@ -63,7 +63,7 @@ object DatabaseFactory { /** * 带有transform */ - suspend fun query(transform: (T) -> R, block: suspend () -> T): R = + suspend fun query(transform: T.() -> R, block: suspend () -> T): R = dbQuery { transform(block()) } /** @@ -71,20 +71,20 @@ object DatabaseFactory { */ suspend fun queryNotNull(transform: T.() -> R?, block: suspend () -> T?): R? = query({ - it?.let { transform(it) } + this?.let { transform(it) } }) { block() } /** * 带有transform */ - suspend fun mapQuery(transform: (T) -> R, block: suspend () -> SizedIterable): List = + suspend fun mapQuery(transform: T.() -> R, block: suspend () -> SizedIterable): List = dbQuery { block().map(transform) } /** * 带有transform */ suspend fun mapQuery( - transform: (R1) -> R, + transform: R1.() -> R, typeTransform: (T) -> R1, block: suspend () -> SizedIterable ): List = @@ -99,7 +99,7 @@ object DatabaseFactory { * @param typeTransform 主要用于将ResultRow 转换成普通数据 */ suspend fun first( - transform: (R1) -> T, + transform: R1.() -> T, typeTransform: (R) -> R1, block: suspend () -> SizedIterable ): T? = dbQuery { diff --git a/client-lib/src/commonMain/kotlin/com/storyteller_f/a/client_lib/Request.kt b/client-lib/src/commonMain/kotlin/com/storyteller_f/a/client_lib/Request.kt index 11ce9d5..1d4dc0c 100644 --- a/client-lib/src/commonMain/kotlin/com/storyteller_f/a/client_lib/Request.kt +++ b/client-lib/src/commonMain/kotlin/com/storyteller_f/a/client_lib/Request.kt @@ -15,6 +15,13 @@ import io.ktor.client.statement.* import io.ktor.http.* suspend fun HttpClient.requestRoomInfo(id: PrimaryKey) = get("room/$id").body() + +suspend fun HttpClient.requestRoomInfoByAid(aid: String) = get("room") { + url { + parameters.append("aid", aid) + } +}.body() + suspend fun HttpClient.requestRoomKeys(id: PrimaryKey) = get("room/$id/pub-keys").body>>() @@ -34,6 +41,12 @@ suspend fun HttpClient.getRoomTopics( suspend fun HttpClient.getCommunityInfo(id: PrimaryKey) = get("community/$id").body() +suspend fun HttpClient.getCommunityInfoByAid(aid: String) = get("community", { + url { + parameters.append("aid", aid) + } +}).body() + suspend fun HttpClient.getCommunityTopics(communityId: PrimaryKey, size: Int) = get("community/$communityId/topics?size=$size").body>() @@ -64,6 +77,17 @@ suspend fun HttpClient.getWorldTopics(nextTopicId: PrimaryKey?, size: Int) = get suspend fun HttpClient.getUserInfo(id: PrimaryKey) = get("user/$id").body() +suspend fun HttpClient.updateUserInfo(newUserInfo: UserInfo) = post("user/update") { + contentType(ContentType.Application.Json) + setBody(newUserInfo) +}.body() + +suspend fun HttpClient.getUserInfoByAid(aid: String) = get("user", { + url { + parameters.append("aid", aid) + } +}).body() + suspend fun HttpClient.getTopicTopics(topicId: PrimaryKey, nextTopicId: PrimaryKey?, size: Int) = get("topic/$topicId/topics") { url { @@ -73,6 +97,12 @@ suspend fun HttpClient.getTopicTopics(topicId: PrimaryKey, nextTopicId: PrimaryK suspend fun HttpClient.getTopicInfo(id: PrimaryKey) = get("topic/$id").body() +suspend fun HttpClient.getTopicInfoByAid(aid: String) = get("topic") { + url { + parameters.append("aid", aid) + } +}.body() + suspend fun HttpClient.getJoinedRooms(size: Int, nextRoomId: PrimaryKey?) = get("room/joined") { url { appendPagingQueryParams(size, nextRoomId) diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityPage.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityPage.kt index 7983a12..273839e 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityPage.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityPage.kt @@ -31,15 +31,13 @@ import com.storyteller_f.a.app.compontents.* import com.storyteller_f.a.app.room.RoomList import com.storyteller_f.a.app.search.CustomSearchBar import com.storyteller_f.a.app.world.TopicList -import com.storyteller_f.a.client_lib.getCommunityInfo -import com.storyteller_f.a.client_lib.getCommunityRooms -import com.storyteller_f.a.client_lib.getCommunityTopics -import com.storyteller_f.a.client_lib.joinCommunity +import com.storyteller_f.a.client_lib.* import com.storyteller_f.shared.model.CommunityInfo import com.storyteller_f.shared.model.RoomInfo import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey +import io.ktor.client.* import kotlinx.coroutines.launch import moe.tlaster.precompose.viewmodel.viewModel import moe.tlaster.precompose.viewmodel.viewModelScope @@ -47,13 +45,22 @@ import org.jetbrains.compose.resources.stringResource data class OnCommunityJoined(val communityId: PrimaryKey) -class CommunityViewModel(private val communityId: PrimaryKey) : SimpleViewModel() { +class CommunityViewModel(private val requestInfo: suspend HttpClient.() -> CommunityInfo) : + SimpleViewModel() { + constructor(communityId: PrimaryKey) : this({ + getCommunityInfo(communityId) + }) + + constructor(communityAid: String) : this({ + getCommunityInfoByAid(communityAid) + }) + init { load() viewModelScope.launch { for (i in bus) { if (i is OnCommunityJoined) { - if (i.communityId == communityId) { + if (i.communityId == handler.data.value?.id) { handler.refresh() } } @@ -64,7 +71,7 @@ class CommunityViewModel(private val communityId: PrimaryKey) : SimpleViewModel< override suspend fun loadInternal() { handler.request { serviceCatching { - client.getCommunityInfo(communityId) + requestInfo(client) } } } diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityRefCell.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityRefCell.kt index 0ee17a2..57dd4d1 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityRefCell.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/community/CommunityRefCell.kt @@ -14,3 +14,13 @@ fun CommunityRefCell(communityId: PrimaryKey, onClick: (PrimaryKey) -> Unit) { CommunityCell(it, onClick) } } + +@Composable +fun CommunityRefCell(communityAid: String, onClick: (PrimaryKey) -> Unit) { + val viewModel = viewModel(CommunityViewModel::class, keys = listOf("community", communityAid)) { + CommunityViewModel(communityAid) + } + StateView2(viewModel.handler) { + CommunityCell(it, onClick) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomPage.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomPage.kt index 74d571a..13d7d25 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomPage.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomPage.kt @@ -45,6 +45,7 @@ import com.storyteller_f.shared.obj.ServerResponse import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey import io.github.aakira.napier.Napier +import io.ktor.client.* import io.ktor.client.plugins.websocket.* import kotbase.Expression import kotbase.MutableDocument @@ -153,12 +154,19 @@ class RoomTopicsRemoteMediator( } } -class RoomViewModel(private val roomId: PrimaryKey) : SimpleViewModel() { +class RoomViewModel(private val requestInfo: suspend HttpClient.() -> RoomInfo) : SimpleViewModel() { + constructor(roomId: PrimaryKey) : this({ + requestRoomInfo(roomId) + }) + constructor(roomAid: String) : this({ + requestRoomInfoByAid(roomAid) + }) + init { load() viewModelScope.launch { for (i in bus) { - if (i is OnRoomJoined && i.id == roomId) { + if (i is OnRoomJoined && i.id == handler.data.value?.id) { handler.refresh() } } @@ -168,7 +176,7 @@ class RoomViewModel(private val roomId: PrimaryKey) : SimpleViewModel( override suspend fun loadInternal() { handler.request { serviceCatching { - client.requestRoomInfo(roomId) + requestInfo(client) } } } diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomRefCell.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomRefCell.kt index b0a234f..eacd564 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomRefCell.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/RoomRefCell.kt @@ -14,3 +14,13 @@ fun RoomRefCell(roomId: PrimaryKey, onClick: (PrimaryKey) -> Unit) { RoomCell(it, onClick) } } + +@Composable +fun RoomRefCell(roomAid: String, onClick: (PrimaryKey) -> Unit) { + val viewModel = viewModel(RoomViewModel::class, keys = listOf("room", roomAid)) { + RoomViewModel(roomAid) + } + StateView2(viewModel.handler) { + RoomCell(it, onClick) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicCell.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicCell.kt index 4db860b..23261e8 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicCell.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicCell.kt @@ -47,6 +47,17 @@ import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.ast.getTextInNode +class TopicRoute(val pattern: String, val builder: @Composable () -> Unit) + +// private val ROUTE = mutableListOf( +// TopicRoute("/topic/{id}") { +// }, +// TopicRoute("/root/{id}") { +// }, +// TopicRoute("/community/{id}") { +// } +// ) + @Composable fun TopicCell( topicInfo: TopicInfo?, @@ -123,7 +134,18 @@ fun TopicRefCell(topicId: PrimaryKey, onClick: (PrimaryKey) -> Unit) { } StateView2(viewModel.handler) { - TopicRefCellContent(it, onClick, topicId) + TopicRefCellContent(it, onClick) + } +} + +@Composable +fun TopicRefCell(topicAid: String, onClick: (PrimaryKey) -> Unit) { + val viewModel = viewModel(TopicViewModel::class, keys = listOf("topic", topicAid)) { + TopicViewModel(topicAid) + } + + StateView2(viewModel.handler) { + TopicRefCellContent(it, onClick) } } @@ -131,7 +153,6 @@ fun TopicRefCell(topicId: PrimaryKey, onClick: (PrimaryKey) -> Unit) { private fun TopicRefCellContent( it: TopicInfo, onClick: (PrimaryKey) -> Unit, - topicId: PrimaryKey, ) { val author = it.author val authorViewModel = viewModel(UserViewModel::class, keys = listOf("user", author)) { @@ -141,7 +162,7 @@ private fun TopicRefCellContent( Row( modifier = Modifier.fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceContainer, RoundedCornerShape(4.dp)).clickable { - onClick(topicId) + onClick(it.id) }.padding(10.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically @@ -187,22 +208,55 @@ private fun RefBlock( val textInNode = readFenceContent(children, langOffset, content) val uri = Uri.parse(textInNode) return if (uri.pathSegments.size == 2) { - val id = uri.pathSegments[1].toULong() val p1 = uri.pathSegments[0] when (p1) { - "topic" -> TopicRefCell(id) { - onClick(it, ObjectType.TOPIC) + "topic" -> { + val id = uri.pathSegments[1].toULongOrNull() + if (id != null) { + TopicRefCell(id) { + onClick(it, ObjectType.TOPIC) + } + } else { + TopicRefCell(uri.pathSegments[1]) { + onClick(it, ObjectType.TOPIC) + } + } } - "room" -> RoomRefCell(roomId = id) { - onClick(it, ObjectType.ROOM) + "room" -> { + val id = uri.pathSegments[1].toULongOrNull() + if (id != null) { + RoomRefCell(id) { + onClick(it, ObjectType.ROOM) + } + } else { + RoomRefCell(uri.pathSegments[1]) { + onClick(it, ObjectType.ROOM) + } + } } - "community" -> CommunityRefCell(communityId = id) { - onClick(it, ObjectType.COMMUNITY) + "community" -> { + val id = uri.pathSegments[1].toULongOrNull() + if (id != null) { + CommunityRefCell(id) { + onClick(it, ObjectType.COMMUNITY) + } + } else { + CommunityRefCell(uri.pathSegments[1]) { + onClick(it, ObjectType.COMMUNITY) + } + } } - "user" -> UserRefCell(userId = id) + "user" -> { + val id = uri.pathSegments[1].toULongOrNull() + if (id != null) { + UserRefCell(userId = id) + } else { + UserRefCell(userAid = uri.pathSegments[1]) + } + } else -> null } diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicPage.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicPage.kt index 60213ac..50a94cc 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicPage.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicPage.kt @@ -19,10 +19,7 @@ import com.storyteller_f.a.app.client import com.storyteller_f.a.app.common.* import com.storyteller_f.a.app.compontents.ReactionRow import com.storyteller_f.a.app.search.CustomSearchBar -import com.storyteller_f.a.client_lib.ClientSession -import com.storyteller_f.a.client_lib.LoginViewModel -import com.storyteller_f.a.client_lib.getTopicInfo -import com.storyteller_f.a.client_lib.getTopicTopics +import com.storyteller_f.a.client_lib.* import com.storyteller_f.shared.decrypt import com.storyteller_f.shared.getDerPrivateKey import com.storyteller_f.shared.model.TopicContent @@ -30,6 +27,7 @@ import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.obj.ServerResponse import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey +import io.ktor.client.* import moe.tlaster.precompose.viewmodel.viewModel @Composable @@ -83,7 +81,15 @@ fun TopicPage(topicId: PrimaryKey, onClick: (PrimaryKey, ObjectType) -> Unit) { } } -class TopicViewModel(private val topicId: PrimaryKey) : SimpleViewModel() { +class TopicViewModel(private val requestInfo: suspend HttpClient.() -> TopicInfo) : SimpleViewModel() { + constructor(topicId: PrimaryKey) : this({ + getTopicInfo(topicId) + }) + + constructor(topicAid: String) : this({ + getTopicInfoByAid(topicAid) + }) + init { load() } @@ -91,7 +97,7 @@ class TopicViewModel(private val topicId: PrimaryKey) : SimpleViewModel() { +@Composable +fun UserRefCell(modifier: Modifier = Modifier, userAid: String) { + val viewModel = viewModel(UserViewModel::class, keys = listOf("user", userAid)) { + UserViewModel(userAid) + } + + StateView2(viewModel.handler) { + Box( + modifier.background(MaterialTheme.colorScheme.secondaryContainer, RoundedCornerShape(10.dp)).padding(10.dp) + ) { + UserHeadRow(it) + } + } +} + +class UserViewModel(private val requestInfo: suspend HttpClient.() -> UserInfo) : SimpleViewModel() { + constructor(userId: PrimaryKey) : this({ + getUserInfo(userId) + }) + constructor(userAid: String) : this({ + getUserInfoByAid(userAid) + }) + init { load() } @@ -41,7 +65,7 @@ class UserViewModel(private val userId: PrimaryKey) : SimpleViewModel( override suspend fun loadInternal() { handler.request { serviceCatching { - client.getUserInfo(userId) + requestInfo(client) } } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/ProtectedContent.kt b/server/src/main/kotlin/com/storyteller_f/a/server/ProtectedContent.kt index d108cc4..1ae13e1 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/ProtectedContent.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/ProtectedContent.kt @@ -16,11 +16,22 @@ fun Route.protectedContent(backend: Backend) { bindProtectedRoomRoute(backend) bindProtectedCommunityRoute(backend) bindProtectedTopicRoute(backend) + bindProtectedUserRoute() webSocket("/link") { webSocketContent(backend) } } +private fun Route.bindProtectedUserRoute() { + route("/user") { + post("/update") { + usePrincipal { id -> + updateUser(id) + } + } + } +} + private fun Route.bindProtectedTopicRoute(backend: Backend) { route("/topic") { post { @@ -51,7 +62,7 @@ private fun Route.bindProtectedCommunityRoute(backend: Backend) { } post("/{id}/join") { usePrincipal { id -> - checkParameter("id") { + checkParameter("id") { joinCommunity(id, it) } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/UnprotectedContent.kt b/server/src/main/kotlin/com/storyteller_f/a/server/UnprotectedContent.kt index 37353a1..a2025b8 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/UnprotectedContent.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/UnprotectedContent.kt @@ -7,7 +7,10 @@ import com.storyteller_f.a.server.common.checkParameter import com.storyteller_f.a.server.common.checkQueryParameter import com.storyteller_f.a.server.common.pagination import com.storyteller_f.a.server.service.* -import com.storyteller_f.shared.model.* +import com.storyteller_f.shared.model.CommunityInfo +import com.storyteller_f.shared.model.RoomInfo +import com.storyteller_f.shared.model.TopicInfo +import com.storyteller_f.shared.model.UserInfo import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey import io.ktor.server.routing.* @@ -34,6 +37,13 @@ fun Route.unProtectedContent(backend: Backend) { private fun Route.bindUserRoute(backend: Backend) { route("/user") { + get { + omitPrincipal { + checkQueryParameter("aid") { + getUserByAid(it, backend) + } + } + } get("/{id}") { omitPrincipal { checkParameter("id") { @@ -113,6 +123,13 @@ private fun Route.bindCommunityRoute(backend: Backend) { } } } + get { + omitPrincipal { + checkQueryParameter("aid") { + getCommunityByAid(it, backend) + } + } + } get("/search") { omitPrincipal { pagination({ @@ -129,6 +146,13 @@ private fun Route.bindCommunityRoute(backend: Backend) { private fun Route.bindRoomRoute(backend: Backend) { route("/room") { + get { + usePrincipalOrNull { uid -> + checkQueryParameter { + getRoom(null, it, uid, backend) + } + } + } get("/{id}/topics") { usePrincipalOrNull { uid -> pagination({ @@ -144,7 +168,7 @@ private fun Route.bindRoomRoute(backend: Backend) { get("/{roomId}") { usePrincipalOrNull { uid -> checkParameter("roomId") { - getRoom(it, uid, backend) + getRoom(it, null, uid, backend) } } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/auth/Auth.kt b/server/src/main/kotlin/com/storyteller_f/a/server/auth/Auth.kt index 9be4be4..704cf54 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/auth/Auth.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/auth/Auth.kt @@ -156,8 +156,8 @@ private suspend fun RoutingContext.signIn(backend: Backend) { val pack = call.receive() val data = call.getData() val f = finalData(data) - val userTriple = DatabaseFactory.first({ user -> - Triple(user.toUserInfo(), user.icon, user.publicKey) + val userTriple = DatabaseFactory.first({ + Triple(toUserInfo(), icon, publicKey) }, User::wrapRow) { User.find { Users.address eq pack.ad @@ -191,7 +191,7 @@ private suspend fun RoutingContext.signUp(backend: Backend) { val newId = SnowflakeFactory.nextId() val name = backend.nameService.parse(newId) call.respond(DatabaseFactory.query({ - it.toUserInfo() to null + toUserInfo() to null }) { createUser(User(null, pack.pk, ad, null, name, newId, now())) }.let { toFinalUserInfo(it, backend) }) @@ -236,7 +236,7 @@ private suspend fun ApplicationCall.verifySignature( sig.isNotBlank() && session.data.isNotBlank() -> { DatabaseFactory.first({ - it + this }, { it[Users.publicKey] }) { diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/auth/UsePrincipal.kt b/server/src/main/kotlin/com/storyteller_f/a/server/auth/UsePrincipal.kt index 4f98421..37c660f 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/auth/UsePrincipal.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/auth/UsePrincipal.kt @@ -58,6 +58,10 @@ suspend fun RoutingContext.respondError(e: Throwable) { e.localizedMessage ) + is BadRequestException -> call.respond( + HttpStatusCode.BadRequest, + e.localizedMessage + ) else -> call.respond(HttpStatusCode.InternalServerError, e.localizedMessage ?: e.toString()) } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/service/Community.kt b/server/src/main/kotlin/com/storyteller_f/a/server/service/Community.kt index ae0cd18..5620f37 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/service/Community.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/service/Community.kt @@ -9,6 +9,8 @@ import com.storyteller_f.shared.utils.now import com.storyteller_f.tables.* import kotlinx.datetime.LocalDateTime import org.jetbrains.exposed.sql.JoinType +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.SizedIterable import org.jetbrains.exposed.sql.selectAll fun Community.toCommunityIfo( @@ -26,27 +28,39 @@ fun Community.toCommunityIfo( suspend fun getCommunity(communityId: PrimaryKey, backend: Backend): Result { return runCatching { - DatabaseFactory.first({ item -> - Triple(item.toCommunityIfo(now()), item.icon, item.poster) - }, { - Community.wrapRow(it) - }) { + getCommunityInternal(backend) { Community.findById(communityId) - }?.let { (info, iconName, coverName) -> - val (iconUrl, coverUrl) = backend.mediaService.get("apic", listOf(iconName, coverName)) - info.copy(icon = getMediaInfo(iconUrl), poster = getMediaInfo(coverUrl)) } } } +suspend fun getCommunityByAid(communityAid: String, backend: Backend): Result { + return runCatching { + getCommunityInternal(backend) { + Community.find { + Communities.aid eq communityAid + } + } + } +} + +private suspend fun getCommunityInternal(backend: Backend, searchCommunity: suspend () -> SizedIterable) = + DatabaseFactory.first({ + Triple(toCommunityIfo(now()), icon, poster) + }, { + Community.wrapRow(it) + }, searchCommunity)?.let { (info, iconName, coverName) -> + val (iconUrl, coverUrl) = backend.mediaService.get("apic", listOf(iconName, coverName)) + info.copy(icon = getMediaInfo(iconUrl), poster = getMediaInfo(coverUrl)) + } + suspend fun joinCommunity( - id: PrimaryKey, - it: PrimaryKey + uid: PrimaryKey, + communityId: PrimaryKey ) = runCatching { DatabaseFactory.dbQuery { - createCommunityJoin(id, it) - } - Unit + createCommunityJoin(uid, communityId) + }.insertedCount > 0 } suspend fun searchCommunities( @@ -57,8 +71,8 @@ suspend fun searchCommunities( size: Int ): Result, Long>> { return runCatching { - val list = DatabaseFactory.mapQuery({ community -> - Triple(community.toCommunityIfo(null), community.icon, community.poster) + val list = DatabaseFactory.mapQuery({ + Triple(toCommunityIfo(null), icon, poster) }, Community::wrapRow) { val query = Community.find { Communities.name like "%$word%" @@ -82,7 +96,8 @@ suspend fun searchJoinedCommunities( size: Int ): Result, Long>> { return runCatching { - val list = DatabaseFactory.mapQuery({ (community, joinTime) -> + val list = DatabaseFactory.mapQuery({ + val (community, joinTime) = this Triple(community.toCommunityIfo(joinTime), community.icon, community.poster) }, { val community = Community.wrapRow(it) diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/service/Room.kt b/server/src/main/kotlin/com/storyteller_f/a/server/service/Room.kt index 5008a1a..e03da04 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/service/Room.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/service/Room.kt @@ -6,6 +6,7 @@ import com.storyteller_f.a.server.common.bindPaginationQuery import com.storyteller_f.shared.model.RoomInfo import com.storyteller_f.shared.type.PrimaryKey import com.storyteller_f.tables.* +import io.ktor.server.plugins.* import kotlinx.datetime.LocalDateTime import org.jetbrains.exposed.sql.* @@ -167,8 +168,8 @@ suspend fun searchRoomInCommunity( ): Result, Long>> { return runCatching { val list = DatabaseFactory.mapQuery({ - val joinedTime = it.getOrNull(RoomJoins.joinTime) - val room = Room.wrapRow(it) + val joinedTime = getOrNull(RoomJoins.joinTime) + val room = Room.wrapRow(this) room.toRoomInfo(joinedTime, communityId) to room.icon }) { val join = Rooms.join(CommunityRooms, JoinType.INNER, Rooms.id, CommunityRooms.roomId) @@ -222,15 +223,25 @@ private fun Room.toRoomInfo(joinedTime: LocalDateTime?, communityId: PrimaryKey? communityId ) -suspend fun getRoom(roomId: PrimaryKey, uid: PrimaryKey?, backend: Backend): Result { +suspend fun getRoom(roomId: PrimaryKey?, roomAid: String?, uid: PrimaryKey?, backend: Backend): Result { + if (roomId == null && roomAid == null) { + return Result.failure(BadRequestException("roomId or roomAid must be set.")) + } return runCatching { - DatabaseFactory.dbQuery { + DatabaseFactory.first({ + this + }, ::mapRoomInfo) { val baseJoin = Rooms.join(CommunityRooms, JoinType.LEFT, Rooms.id, CommunityRooms.roomId) val baseOp = Op.build { - Rooms.id eq roomId + if (roomId != null) { + Rooms.id eq roomId + } else { + Rooms.aid eq roomAid!! + } } val baseFields = Rooms.fields + CommunityRooms.communityId if (uid != null) { + // 检查用户是否加入,查询加入时间 baseJoin .join(RoomJoins, JoinType.INNER, Rooms.id, RoomJoins.roomId) .select(baseFields + RoomJoins.joinTime) @@ -243,7 +254,7 @@ suspend fun getRoom(roomId: PrimaryKey, uid: PrimaryKey?, backend: Backend): Res .where { baseOp } - }.map(::mapRoomInfo).firstOrNull() + } }?.let { val (info, iconName) = it val icon = backend.mediaService.get("apic", listOf(iconName)).firstOrNull() diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/service/User.kt b/server/src/main/kotlin/com/storyteller_f/a/server/service/User.kt index df68ae7..b990ade 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/service/User.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/service/User.kt @@ -6,7 +6,12 @@ import com.storyteller_f.shared.model.MediaInfo import com.storyteller_f.shared.model.UserInfo import com.storyteller_f.shared.type.PrimaryKey import com.storyteller_f.tables.User +import com.storyteller_f.tables.Users +import io.ktor.server.plugins.* +import io.ktor.server.request.* import io.ktor.server.routing.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.update fun User.toUserInfo(): UserInfo { return UserInfo(id, address, 0, aid, nickname, null) @@ -30,3 +35,43 @@ suspend fun RoutingContext.getUser( User.findById(it) }?.let { toFinalUserInfo(it, backend) } } + +suspend fun RoutingContext.getUserByAid( + aid: String, + backend: Backend +) = runCatching { + DatabaseFactory.first({ + toUserInfo() to icon + }, User::wrapRow) { + User.find { + Users.aid eq aid + } + }?.let { toFinalUserInfo(it, backend) } +} + +suspend fun RoutingContext.updateUser(id: PrimaryKey) = + runCatching { + val newUser = call.receive() + if (!newUser.aid.isNullOrBlank()) { + // check aid is null + if (DatabaseFactory.queryNotNull({ + aid + }) { + User.findById(id) + } != null) { + throw BadRequestException("aid is not null.") + } + } + DatabaseFactory.dbQuery { + Users.update({ + Users.id eq id + }) { + if (newUser.nickname.isNotBlank()) { + it[nickname] = newUser.nickname + } + if (!newUser.aid.isNullOrBlank()) { + it[aid] = newUser.aid + } + } + } + } diff --git a/server/src/test/kotlin/CommunityTest.kt b/server/src/test/kotlin/CommunityTest.kt index 745e9f4..19992c5 100644 --- a/server/src/test/kotlin/CommunityTest.kt +++ b/server/src/test/kotlin/CommunityTest.kt @@ -4,36 +4,36 @@ import com.storyteller_f.a.client_lib.* import com.storyteller_f.buildBackendFromEnv import com.storyteller_f.naming.NameService import com.storyteller_f.readEnv -import com.storyteller_f.shared.* +import com.storyteller_f.shared.hmacSign +import com.storyteller_f.shared.hmacVerify import com.storyteller_f.shared.model.CommunityInfo import com.storyteller_f.shared.model.TopicContent import com.storyteller_f.shared.model.TopicInfo +import com.storyteller_f.shared.newHmacSha512 import com.storyteller_f.shared.obj.NewTopic import com.storyteller_f.shared.obj.TopicSnapshotPack import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey import com.storyteller_f.shared.utils.now import com.storyteller_f.tables.Community -import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.config.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.server.testing.* import kotlinx.coroutines.runBlocking -import java.nio.file.Paths -import kotlin.io.path.ExperimentalPathApi -import kotlin.io.path.deleteRecursively import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertTrue class CommunityTest { + @Test + fun `test get community`() = test { client -> + val communityId = createCommunity() + val community = client.getCommunityInfo(communityId) + assertEquals(communityId, client.getCommunityInfoByAid(community.aid).id) + } + @Test fun `test create topic in community`() = test { client -> session(client) { @@ -147,49 +147,3 @@ class CommunityTest { } } } - -@Suppress("unused") -fun Application.module() { - log.info("Hello from test!") - routing { - get { - call.respond(HttpStatusCode.OK, "Hello, world!") - } - } -} - -@OptIn(ExperimentalPathApi::class) -fun test(block: suspend (HttpClient) -> Unit) { - SnowflakeFactory.setMachine(0) - testApplication { - addProvider() - val path = Paths.get("../deploy/lucene_data/index") - val backend = buildBackendFromEnv(readEnv()) - DatabaseFactory.init(backend.config.databaseConnection) - environment { - config = MapApplicationConfig( - "ktor.application.modules.0" to "CommunityTestKt.module", - "ktor.application.modules.1" to "com.storyteller_f.a.server.ApplicationKt.module", - "ktor.application.modules.size" to "2" - ) - } - val client = createClient { - defaultClientConfigure() - } - block(client) - path.deleteRecursively() - DatabaseFactory.clean() - } -} - -suspend fun session(client: HttpClient, block: suspend () -> Unit) { - val priKey = generateKeyPair() - val pubKey = getDerPublicKeyFromPrivateKey(priKey) - val address = calcAddress(pubKey) - val data = finalData(client.getData()) - val sign = signature(priKey, data) - val userInfo = client.sign(true, pubKey, sign, address) - LoginViewModel.updateState(ClientSession.LoginSuccess(priKey, pubKey, address)) - LoginViewModel.updateUser(userInfo) - block() -} diff --git a/server/src/test/kotlin/TestBuilder.kt b/server/src/test/kotlin/TestBuilder.kt new file mode 100644 index 0000000..62e8aed --- /dev/null +++ b/server/src/test/kotlin/TestBuilder.kt @@ -0,0 +1,56 @@ +import com.perraco.utils.SnowflakeFactory +import com.storyteller_f.DatabaseFactory +import com.storyteller_f.a.client_lib.* +import com.storyteller_f.buildBackendFromEnv +import com.storyteller_f.readEnv +import com.storyteller_f.shared.* +import io.ktor.client.* +import io.ktor.server.application.* +import io.ktor.server.config.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import java.nio.file.Paths +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively + +@Suppress("unused") +fun Application.module() { + routing { + } +} + +@OptIn(ExperimentalPathApi::class) +fun test(block: suspend (HttpClient) -> Unit) { + SnowflakeFactory.setMachine(0) + testApplication { + addProvider() + val path = Paths.get("../deploy/lucene_data/index") + val backend = buildBackendFromEnv(readEnv()) + DatabaseFactory.init(backend.config.databaseConnection) + environment { + config = MapApplicationConfig( + "ktor.application.modules.0" to "TestBuilderKt.module", + "ktor.application.modules.1" to "com.storyteller_f.a.server.ApplicationKt.module", + "ktor.application.modules.size" to "2" + ) + } + val client = createClient { + defaultClientConfigure() + } + block(client) + path.deleteRecursively() + DatabaseFactory.clean() + } +} + +suspend fun session(client: HttpClient, block: suspend () -> Unit) { + val priKey = generateKeyPair() + val pubKey = getDerPublicKeyFromPrivateKey(priKey) + val address = calcAddress(pubKey) + val data = finalData(client.getData()) + val sign = signature(priKey, data) + val userInfo = client.sign(true, pubKey, sign, address) + LoginViewModel.updateState(ClientSession.LoginSuccess(priKey, pubKey, address)) + LoginViewModel.updateUser(userInfo) + block() +} diff --git a/server/src/test/kotlin/UserTest.kt b/server/src/test/kotlin/UserTest.kt new file mode 100644 index 0000000..c398918 --- /dev/null +++ b/server/src/test/kotlin/UserTest.kt @@ -0,0 +1,27 @@ +import com.storyteller_f.a.client_lib.LoginViewModel +import com.storyteller_f.a.client_lib.getUserInfo +import com.storyteller_f.a.client_lib.getUserInfoByAid +import com.storyteller_f.a.client_lib.updateUserInfo +import com.storyteller_f.shared.model.UserInfo +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class UserTest { + @Test + fun `test get user`() = test { client -> + session(client) { + val uid = LoginViewModel.user.value?.id + assertNotNull(uid) + val aid = client.getUserInfo(uid).aid + assertNull(aid) + val updateRow = client.updateUserInfo( + UserInfo.EMPTY.copy(aid = "newaid") + ) + assertEquals(1, updateRow) + val user = client.getUserInfoByAid("newaid") + assertEquals(uid, user.id) + } + } +}