From 7fca3440c0a3294010f0a5a503b4b40b8d0b3e78 Mon Sep 17 00:00:00 2001 From: storytellerF <34095089+storytellerF@users.noreply.github.com> Date: Thu, 26 Dec 2024 00:02:34 +0800 Subject: [PATCH] fix ip reader, fix extract --- .../com/storyteller_f/tables/Reactions.kt | 9 +- .../com/storyteller_f/a/client_lib/Request.kt | 25 +++-- .../kotlin/com/storyteller_f/a/app/App.kt | 22 +++- .../a/app/compontents/CommunityIcon.kt | 2 +- .../a/app/compontents/Reaction.kt | 4 +- .../a/app/compontents/UserIcon.kt | 13 ++- .../com/storyteller_f/a/app/model/Build.kt | 12 +- .../com/storyteller_f/a/app/model/Models.kt | 80 ++++++++------ .../storyteller_f/a/app/room/MyRoomsPage.kt | 3 +- .../storyteller_f/a/app/topic/CodeFence.kt | 15 ++- .../storyteller_f/a/app/topic/TopicCell.kt | 103 ++++++++++-------- .../a/app/topic/TopicComposePage.kt | 12 +- .../storyteller_f/a/app/topic/TopicPage.kt | 16 +-- deploy/docker-compose.yml | 1 + .../com/storyteller_f/a/server/auth/Auth.kt | 14 +-- .../a/server/auth/UsePrincipal.kt | 29 +++-- .../com/storyteller_f/a/server/route/Route.kt | 33 +++--- .../a/server/route/SafeCommunityRoute.kt | 17 +-- .../a/server/route/SafeMediaRoute.kt | 11 +- .../a/server/route/SafeRoomRoute.kt | 21 ++-- .../a/server/route/SafeTopicRoute.kt | 35 +++--- .../a/server/route/SafeUserRoute.kt | 13 ++- server/src/test/kotlin/TopicTest.kt | 7 +- .../storyteller_f/shared/model/TopicInfo.kt | 4 + .../storyteller_f/shared/obj/NewReaction.kt | 4 + 25 files changed, 287 insertions(+), 218 deletions(-) diff --git a/backend/src/main/kotlin/com/storyteller_f/tables/Reactions.kt b/backend/src/main/kotlin/com/storyteller_f/tables/Reactions.kt index 04998cf..6ee24c4 100644 --- a/backend/src/main/kotlin/com/storyteller_f/tables/Reactions.kt +++ b/backend/src/main/kotlin/com/storyteller_f/tables/Reactions.kt @@ -104,22 +104,23 @@ suspend fun getReaction(uid: PrimaryKey, objectId: PrimaryKey, emojiText: String } } -suspend fun getSingleReaction(uid: PrimaryKey, emoji: String): Result { +suspend fun getSingleReaction(uid: PrimaryKey, emoji: String, objectId: PrimaryKey): Result { return DatabaseFactory.first({ SingleReactionInfo(id, emoji, objectId, objectType, createdTime, uid) }, { Reaction.wrapRow(it) }) { Reactions.selectAll().where { - (Reactions.emoji eq emoji) and (Reactions.uid eq uid) + (Reactions.objectId eq objectId) and (Reactions.uid eq uid) and (Reactions.emoji eq emoji) } } } suspend fun deleteReaction( uid: PrimaryKey, - emoji: String -): Result = getSingleReaction(uid, emoji).mapResult { + emoji: String, + objectId: PrimaryKey +): Result = getSingleReaction(uid, emoji, objectId).mapResult { if (it == null) { Result.success(true) } else { 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 859892d..d3a059e 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 @@ -75,6 +75,9 @@ suspend fun HttpClient.getRoomTopics( ) = serviceCatching { get("rooms/$roomId/topics") { url { + if (isAlreadyLogin()) { + parameters.append("fillHasCommented", "true") + } appendPagingQueryParams(size, nextTopicId) } }.body>() @@ -171,20 +174,15 @@ private fun URLBuilder.appendPagingQueryParams(size: Int, nextId: PrimaryKey?) { } } -private fun URLBuilder.appendPagingQueryParams(size: Int, nextId: String?) { - parameters.append("size", size.toString()) - if (nextId != null) { - parameters.append("nextPageToken", nextId) - } -} - -suspend fun HttpClient.getWorldTopics(nextTopicId: PrimaryKey?, size: Int, fillHasCommented: Boolean) = +suspend fun HttpClient.getWorldTopics(nextTopicId: PrimaryKey?, size: Int) = serviceCatching { get( "topics/recommend" ) { url { - parameters.append("fillHasCommented", fillHasCommented.toString()) + if (isAlreadyLogin()) { + parameters.append("fillHasCommented", "true") + } appendPagingQueryParams(size, nextTopicId) } }.body>() @@ -213,6 +211,9 @@ suspend fun HttpClient.getTopicTopics(topicId: PrimaryKey, nextTopicId: PrimaryK serviceCatching { get("topics/$topicId/topics") { url { + if (isAlreadyLogin()) { + parameters.append("fillHasCommented", "true") + } appendPagingQueryParams(size, nextTopicId) } }.body>() @@ -331,10 +332,10 @@ suspend fun HttpClient.addReaction(topicId: PrimaryKey, emoji: String) = service }.body() } -suspend fun HttpClient.deleteReaction(emoji: String) = serviceCatching { +suspend fun HttpClient.deleteReaction(emoji: String, objectId: PrimaryKey) = serviceCatching { post("reactions/delete") { contentType(ContentType.Application.Json) - setBody(NewReaction(emoji)) + setBody(DeleteReaction(emoji, objectId)) }.body() } @@ -379,7 +380,7 @@ suspend fun HttpClient.upload( formData { append("description", "amedia") append("file", stream, Headers.build { - append(HttpHeaders.ContentType, ContentType.defaultForFileExtension(extension).contentType) + append(HttpHeaders.ContentType, ContentType.defaultForFileExtension(extension)) append(HttpHeaders.ContentDisposition, "filename=\"$name\"") }) }, diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/App.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/App.kt index ea2e388..c884fe7 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/App.kt @@ -37,6 +37,7 @@ import com.storyteller_f.a.client_lib.LoginViewModel import com.storyteller_f.a.client_lib.addRequestHeaders import com.storyteller_f.a.client_lib.defaultClientConfigure import com.storyteller_f.a.client_lib.getClient +import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.obj.RoomFrame import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey @@ -232,12 +233,7 @@ val wsClient by lazy { }) { if (it is RoomFrame.NewTopicInfo) { val info = processEncryptedTopic(listOf(it.topicInfo)).first() - getOrCreateCollection("topics${info.parentId}").save( - MutableDocument( - info.id.toString(), - Json.encodeToString(info) - ) - ) + updateDocumentInParent(info) Napier.v(tag = "pagination") { "save document $info" } @@ -245,6 +241,20 @@ val wsClient by lazy { } } +fun updateDocumentInParent(info: TopicInfo) { + val collectionName = "topics${info.parentId}" + updateDocument(collectionName, info) +} + +fun updateDocument(collectionName: String, info: TopicInfo) { + getOrCreateCollection(collectionName).save( + MutableDocument( + info.id.toString(), + Json.encodeToString(info) + ) + ) +} + fun HttpClientConfig<*>.setupRequest() { defaultRequest { url(BuildKonfig.SERVER_URL) diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/CommunityIcon.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/CommunityIcon.kt index fe832f3..58b9867 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/CommunityIcon.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/CommunityIcon.kt @@ -28,7 +28,7 @@ fun CommunityIcon( val radius = 8.dp val shape = RoundedCornerShape(radius) if (model != null) { - AsyncImage(model, contentDescription = null, Modifier.size(iconSize).clip(shape).clickable { + AsyncImage(globalLoader(model), contentDescription = null, Modifier.size(iconSize).clip(shape).clickable { updateDialog(true) }) } else { diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/Reaction.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/Reaction.kt index 83b6e43..d5fd686 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/Reaction.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/Reaction.kt @@ -122,10 +122,10 @@ private fun EmojiCell( val hasReacted = info.hasReacted Pill(info.count.toString(), emoji = emoji, selected = hasReacted == true) { emoji.let { string -> - if (hasReacted == true) { + if (hasReacted) { scope.launch { globalDialogState.use { - client.deleteReaction(string) + client.deleteReaction(string, topicInfo.id) bus.emit(OnTopicChanged(topicInfo.copy(reactionCount = reactionCount - 1))) } } diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/UserIcon.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/UserIcon.kt index 92a20b2..b2ec5fa 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/UserIcon.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/compontents/UserIcon.kt @@ -15,7 +15,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.network.ktor3.KtorNetworkFetcherFactory +import coil3.request.ImageRequest +import coil3.request.crossfade import com.storyteller_f.a.app.LocalAppNav +import com.storyteller_f.a.app.client import com.storyteller_f.a.app.user.UserDialog import com.storyteller_f.a.client_lib.LoginViewModel import com.storyteller_f.shared.model.UserInfo @@ -38,7 +43,7 @@ fun UserIcon(userInfo: UserInfo?, showDialog: Boolean = true, size: Dp = 40.dp) val url = userInfo?.avatar?.url if (url != null) { AsyncImage( - url, + globalLoader(url), contentDescription = "${userInfo.nickname}'s avatar", modifier = Modifier.size(size).clip(CircleShape).clickable(showDialog, onClick = onClick) ) @@ -57,3 +62,9 @@ fun UserIcon(userInfo: UserInfo?, showDialog: Boolean = true, size: Dp = 40.dp) showMyDialog = false } } + +@Composable +fun globalLoader(url: String) = + ImageRequest.Builder(LocalPlatformContext.current).data(url).crossfade(true).fetcherFactory( + KtorNetworkFetcherFactory(client) + ).build() diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Build.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Build.kt index cdcf232..6343c73 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Build.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Build.kt @@ -4,7 +4,9 @@ import androidx.compose.runtime.Composable import com.storyteller_f.a.app.common.viewModel import com.storyteller_f.a.app.search.SearchScope import com.storyteller_f.shared.model.RoomInfo +import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.obj.JoinStatusSearch +import com.storyteller_f.shared.type.DEFAULT_PRIMARY_KEY import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey @@ -135,6 +137,12 @@ fun createTopicViewModel(topicId: PrimaryKey) = TopicViewModel(topicId) } +@Composable +fun createTopicViewModelFromInfo(topicInfo: TopicInfo) = + viewModel(keys = listOf("topic", topicInfo.id)) { + TopicViewModel(topicInfo) + } + @Composable fun createTopicsInTopicViewModel(topicId: PrimaryKey) = viewModel(keys = listOf("topic-topics", topicId)) { @@ -202,8 +210,8 @@ fun createUserViewModel(userId: PrimaryKey) = } @Composable -fun createWorldViewModel() = viewModel { - WorldViewModel() +fun createWorldViewModel() = viewModel(keys = listOf("world")) { + TopicsViewModel(DEFAULT_PRIMARY_KEY, null) } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Models.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Models.kt index bfa5716..d6ec2fb 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Models.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/model/Models.kt @@ -10,24 +10,19 @@ import com.storyteller_f.a.app.client import com.storyteller_f.a.app.common.* import com.storyteller_f.a.app.compontents.DialogSaveState import com.storyteller_f.a.app.topic.processEncryptedTopic +import com.storyteller_f.a.app.updateDocument +import com.storyteller_f.a.app.updateDocumentInParent import com.storyteller_f.a.client_lib.* import com.storyteller_f.shared.model.* import com.storyteller_f.shared.obj.JoinStatusSearch import com.storyteller_f.shared.obj.ServerResponse -import com.storyteller_f.shared.type.ObjectType -import com.storyteller_f.shared.type.PrimaryKey -import com.storyteller_f.shared.type.toPrimaryKey -import com.storyteller_f.shared.type.toPrimaryKeyOrNull +import com.storyteller_f.shared.type.* +import com.storyteller_f.shared.utils.extractMarkdownHeadline import io.github.aakira.napier.Napier import io.ktor.client.* import kotbase.Expression -import kotbase.MutableDocument -import kotbase.ktx.all -import kotbase.ktx.orderBy -import kotbase.ktx.select -import kotbase.ktx.where +import kotbase.ktx.* import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json data class OnCommunityJoined(val newInfo: CommunityInfo) @@ -105,7 +100,7 @@ data class OnRoomJoined(val newInfo: RoomInfo) data class OnRoomExited(val newInfo: RoomInfo) @OptIn(ExperimentalPagingApi::class) -class TopicsViewModel(id: PrimaryKey, val type: ObjectType) : PagingViewModel({ +class TopicsViewModel(id: PrimaryKey, val type: ObjectType? = null) : PagingViewModel({ CustomQueryPagingSource( select = select(all()), collectionName = "topics$id", @@ -127,13 +122,42 @@ class TopicsViewModel(id: PrimaryKey, val type: ObjectType) : PagingViewModel - val info = when (type) { - ObjectType.ROOM -> client.getRoomTopics(id, loadKey, 20) - ObjectType.COMMUNITY -> client.getCommunityTopics(id, loadKey, 20) + val info = when { + id == DEFAULT_PRIMARY_KEY -> client.getWorldTopics(loadKey, 10) + type == ObjectType.ROOM -> client.getRoomTopics(id, loadKey, 20) + type == ObjectType.COMMUNITY -> client.getCommunityTopics(id, loadKey, 20) else -> client.getTopicTopics(id, loadKey, 20) }.getOrThrow() - info.copy(processEncryptedTopic(info.data)) -}) + info.copy(processEncryptedTopic(info.data).map { + extractHeadlineIfPlain(it) + }) +}) { + init { + viewModelScope.launch { + bus.collect { value -> + if (value is OnTopicChanged) { + val topicInfo = value.topicInfo + updateDocumentInParent(extractHeadlineIfPlain(topicInfo)) + // 尝试更新到推荐 + if (select(all()).from(getOrCreateCollection("topics$id")) + .where(Expression.property("id").equalTo(topicInfo.id)).execute().next() != null + ) { + updateDocument("topics0", topicInfo) + } + } + } + } + } +} + +private fun extractHeadlineIfPlain(it: TopicInfo): TopicInfo { + val content = it.content + return if (content is TopicContent.Plain) { + it.copy(content = TopicContent.Extracted(extractMarkdownHeadline(content.plain))) + } else { + it + } +} @OptIn(ExperimentalPagingApi::class) class TopicsRemoteMediator( @@ -141,11 +165,6 @@ class TopicsRemoteMediator( val networkService: suspend (PrimaryKey?) -> ServerResponse ) : RemoteMediator() { - private val scope = database.defaultScope - private val collection - get() = scope.getCollection(collectionName) ?: database.createCollection( - collectionName - ) override suspend fun load( loadType: LoadType, @@ -175,17 +194,7 @@ class TopicsRemoteMediator( database.deleteCollection(collectionName) } response.data.forEach { - val rawId = it.id.toString(2) - collection.save( - MutableDocument( - if (rawId.length == 64) { - rawId - } else { - "0$rawId" - }, - Json.encodeToString(it) - ) - ) + updateDocument(collectionName, it) } Napier.v(tag = "pagination") { "mediator success $loadKey" @@ -287,9 +296,12 @@ class UserViewModel(private val requestInfo: suspend HttpClient.() -> Result({ SimplePagingSource { serviceCatching { - client.getWorldTopics(it, 10, LoginViewModel.currentIsAlreadySignUp).getOrThrow() + client.getWorldTopics(it, 10).getOrThrow() }.map { - APagingData(it.data, it.pagination?.nextPageToken?.toPrimaryKeyOrNull()) + val data = it.data.map { info -> + extractHeadlineIfPlain(info) + } + APagingData(data, it.pagination?.nextPageToken?.toPrimaryKeyOrNull()) } } }) diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/MyRoomsPage.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/MyRoomsPage.kt index 1b228d5..fc4614d 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/MyRoomsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/room/MyRoomsPage.kt @@ -21,6 +21,7 @@ import coil3.compose.AsyncImage import com.storyteller_f.a.app.LocalAppNav import com.storyteller_f.a.app.common.StateView import com.storyteller_f.a.app.compontents.CommunityIcon +import com.storyteller_f.a.app.compontents.globalLoader import com.storyteller_f.a.app.compontents.rememberCommonDialogController import com.storyteller_f.a.app.model.createCommunityViewModel import com.storyteller_f.a.app.model.createJoinedRoomsViewModel @@ -122,7 +123,7 @@ fun RoomIcon( val shape = RoundedCornerShape(radius) if (iconUrl != null) { AsyncImage( - iconUrl, + globalLoader(iconUrl), contentDescription = "${roomInfo.name}'s icon", modifier = Modifier.size(size).clip(shape).clickable(enableClick) { updateShowDialog(true) diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/CodeFence.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/CodeFence.kt index c66b96b..5b77d54 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/CodeFence.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/CodeFence.kt @@ -23,11 +23,13 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImagePainter import coil3.compose.LocalPlatformContext import coil3.compose.rememberAsyncImagePainter +import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.request.ImageRequest import com.mikepenz.markdown.compose.components.MarkdownComponentModel import com.mikepenz.markdown.compose.elements.MarkdownHighlightedCodeFence import com.mikepenz.markdown.model.ImageData import com.mikepenz.markdown.model.ImageTransformer +import com.storyteller_f.a.app.client import com.storyteller_f.a.app.compontents.AudioView import com.storyteller_f.a.app.compontents.TextUnitToPx import com.storyteller_f.a.app.compontents.VideoView @@ -190,15 +192,20 @@ private fun readFenceContent( return content.substring(start, end) } +@Composable +private fun imageRequestInMarkdown(link: String, mediaMap: Map) = + ImageRequest.Builder(LocalPlatformContext.current) + .fetcherFactory(KtorNetworkFetcherFactory(client)) + .data(mediaMap[link]?.url) + .size(coil3.size.Size.ORIGINAL) + .build() + class CustomCoil3ImageTransformerImpl(private val mediaMap: Map) : ImageTransformer { @Composable override fun transform(link: String): ImageData { return rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalPlatformContext.current) - .data(mediaMap[link]?.url) - .size(coil3.size.Size.ORIGINAL) - .build() + model = imageRequestInMarkdown(link, mediaMap) ).let { ImageData(it) } } 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 397071a..a786d1a 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 @@ -21,9 +21,9 @@ import com.mikepenz.markdown.m3.markdownTypography import com.storyteller_f.a.app.LocalAppNav import com.storyteller_f.a.app.compontents.InteractionRow import com.storyteller_f.a.app.model.createMediaListViewModel -import com.storyteller_f.a.app.model.createTopicViewModel import com.storyteller_f.a.app.model.createUserViewModel import com.storyteller_f.a.app.user.UserCell +import com.storyteller_f.shared.model.MediaInfo import com.storyteller_f.shared.model.TopicContent import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.model.UserInfo @@ -33,25 +33,21 @@ import org.jetbrains.compose.resources.stringResource @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopicCell( - topicInfoRaw: TopicInfo, + info: TopicInfo, contentAlignAvatar: Boolean = true, showAvatar: Boolean = true ) { - val viewModel = createTopicViewModel(topicInfoRaw.id) - val topicInfo by viewModel.handler.data.collectAsState() - topicInfo?.let { info -> - val author = info.author - val authorViewModel = createUserViewModel(author) + val author = info.author + val authorViewModel = createUserViewModel(author) - val sheetState = rememberModalBottomSheetState() - var showBottomSheet by remember { mutableStateOf(false) } - val authorInfo by authorViewModel.handler.data.collectAsState() - TopicCellInternal(info, showAvatar, authorInfo, contentAlignAvatar) { - showBottomSheet = true - } - EmojiPicker(sheetState, showBottomSheet, info) { - showBottomSheet = false - } + val sheetState = rememberModalBottomSheetState() + var showBottomSheet by remember { mutableStateOf(false) } + val authorInfo by authorViewModel.handler.data.collectAsState() + TopicCellInternal(info, showAvatar, authorInfo, contentAlignAvatar) { + showBottomSheet = true + } + EmojiPicker(sheetState, showBottomSheet, info) { + showBottomSheet = false } } @@ -105,33 +101,16 @@ fun TopicContentField( when (val content = topicInfo.content) { is TopicContent.Plain -> { val isPrivate = topicInfo.isPrivate - val mediaList = if (isPrivate) { - val list = createMediaListViewModel(topicInfo.rootId, 0) - val media by list.handler.data.collectAsState() - media?.data.orEmpty() - } else { - content.list - } - val plain by produceState("", content.plain, showHeadline) { - value = if (showHeadline) { - extractMarkdownHeadline(content.plain) - } else { - content.plain - } - } - val mediaMap = mediaList.associateBy { it.item.name } - Markdown( - plain, - modifier = Modifier.fillMaxWidth().clickable(onClick != null) { - onClick?.invoke() - }, - colors = markdownColor(), - typography = markdownTypography(), - imageTransformer = CustomCoil3ImageTransformerImpl(mediaMap), - components = markdownComponents(codeFence = { - CustomCodeFence(it, mediaMap) - }, codeBlock = { HighlightCodeBlock(it) }) - ) + val rawMediaList = content.list + val plain1 = content.plain + TopicContentFieldInternal(isPrivate, topicInfo, rawMediaList, plain1, showHeadline, onClick) + } + + is TopicContent.Extracted -> { + val isPrivate = topicInfo.isPrivate + val rawMediaList = content.list + val plain1 = content.plain + TopicContentFieldInternal(isPrivate, topicInfo, rawMediaList, plain1, showHeadline, onClick) } is TopicContent.DecryptFailed, is TopicContent.Encrypted -> { @@ -144,3 +123,41 @@ fun TopicContentField( } } } + +@Composable +private fun TopicContentFieldInternal( + isPrivate: Boolean, + topicInfo: TopicInfo, + rawMediaList: List, + plain1: String, + showHeadline: Boolean, + onClick: (() -> Unit)? +) { + val mediaList = if (isPrivate) { + val list = createMediaListViewModel(topicInfo.rootId, 0) + val media by list.handler.data.collectAsState() + media?.data.orEmpty() + } else { + rawMediaList + } + val plain by produceState("", plain1, showHeadline) { + value = if (showHeadline) { + extractMarkdownHeadline(plain1) + } else { + plain1 + } + } + val mediaMap = mediaList.associateBy { it.item.name } + Markdown( + plain, + modifier = Modifier.fillMaxWidth().clickable(onClick != null) { + onClick?.invoke() + }, + colors = markdownColor(), + typography = markdownTypography(), + imageTransformer = CustomCoil3ImageTransformerImpl(mediaMap), + components = markdownComponents(codeFence = { + CustomCodeFence(it, mediaMap) + }, codeBlock = { HighlightCodeBlock(it) }) + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicComposePage.kt b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicComposePage.kt index f4e3969..5bd7d02 100644 --- a/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicComposePage.kt +++ b/composeApp/src/commonMain/kotlin/com/storyteller_f/a/app/topic/TopicComposePage.kt @@ -31,11 +31,11 @@ import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor import com.storyteller_f.a.app.bus import com.storyteller_f.a.app.client import com.storyteller_f.a.app.common.StateView -import com.storyteller_f.a.app.common.getOrCreateCollection import com.storyteller_f.a.app.globalDialogState import com.storyteller_f.a.app.model.MediaListViewModel import com.storyteller_f.a.app.model.OnMediaUploaded import com.storyteller_f.a.app.model.createMediaListViewModel +import com.storyteller_f.a.app.updateDocumentInParent import com.storyteller_f.a.client_lib.LoginViewModel import com.storyteller_f.a.client_lib.createNewTopic import com.storyteller_f.a.client_lib.upload @@ -49,11 +49,8 @@ import io.github.aakira.napier.Napier import io.github.vinceglb.filekit.core.FileKit import io.github.vinceglb.filekit.core.extension import io.github.vinceglb.filekit.core.pickFile -import kotbase.MutableDocument import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import org.jetbrains.compose.resources.stringResource import kotlin.time.Duration.Companion.seconds @@ -360,12 +357,7 @@ private fun TopicComposeSubmitButton( scope.launch { globalDialogState.use { val info = client.createNewTopic(objectType, objectId, finalInput).getOrThrow() - getOrCreateCollection("topics${info.parentId}").save( - MutableDocument( - info.id.toString(), - Json.encodeToString(info) - ) - ) + updateDocumentInParent(info) backPrePage() } } 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 8114c0b..c336e81 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 @@ -26,16 +26,12 @@ import app.cash.paging.compose.LazyPagingItems import app.cash.paging.compose.collectAsLazyPagingItems import com.dokar.sonner.Toaster import com.dokar.sonner.rememberToasterState -import com.storyteller_f.a.app.LocalAppNav -import com.storyteller_f.a.app.bus -import com.storyteller_f.a.app.client +import com.storyteller_f.a.app.* import com.storyteller_f.a.app.common.StateView -import com.storyteller_f.a.app.common.getOrCreateCollection import com.storyteller_f.a.app.common.nestedStateView import com.storyteller_f.a.app.compontents.CustomAlertDialog import com.storyteller_f.a.app.compontents.CustomAlertDialogController import com.storyteller_f.a.app.compontents.InteractionRow -import com.storyteller_f.a.app.globalDialogState import com.storyteller_f.a.app.model.* import com.storyteller_f.a.app.room.CommonInputButton import com.storyteller_f.a.app.room.InputGroupInternal @@ -49,12 +45,9 @@ import com.storyteller_f.shared.model.TopicContent import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey -import kotbase.MutableDocument import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import org.jetbrains.compose.resources.getString import org.kodein.emoji.Emoji import org.kodein.emoji.list @@ -316,12 +309,7 @@ private fun TopicInputGroup( sendState = LoadingState.Loading try { val info = client.createNewTopic(ObjectType.TOPIC, topic.id, input).getOrThrow() - getOrCreateCollection("topics${info.parentId}").save( - MutableDocument( - info.id.toString(), - Json.encodeToString(info) - ) - ) + updateDocumentInParent(info) sendState = LoadingState.Done() input = "" focusManager.clearFocus() diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9b08f2e..feac123 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -75,6 +75,7 @@ services: - REVERSE_PROXY_INTERCEPT_ERRORS=no - USE_BAD_BEHAVIOR=no - BAD_BEHAVIOR_STATUS_CODES=400 403 404 405 429 444 + - USE_MODSECURITY_CRS=no networks: - bw-universe - bw-services 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 b2c4698..7bb99a5 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 @@ -255,14 +255,14 @@ private suspend fun checkDevWsLink(call: ApplicationCall): CustomPrincipal? { } } -private fun ApplicationCall.getData(databaseReader: DatabaseReader): String { - val (_, data) = getSession(databaseReader) +private fun ApplicationCall.getData(reader: DatabaseReader): String { + val (_, data) = getSession(reader) return data } @OptIn(ExperimentalUuidApi::class) -private fun ApplicationCall.getSession(databaseReader: DatabaseReader): Pair { - val remote = remoteIp(databaseReader).first().first +private fun ApplicationCall.getSession(reader: DatabaseReader): Pair { + val remote = remoteIp(reader).first().first return when (val session = sessions.get(UserSession::class)) { null -> { val data = Uuid.random().toString() @@ -323,7 +323,7 @@ fun Application.configureAuth(backend: Backend, reader: DatabaseReader) { } } } - commonRoute(backend) + commonRoute(backend, reader) } fun Route.bindUnprotectedAccountRoute(backend: Backend, databaseReader: DatabaseReader) { @@ -340,9 +340,9 @@ fun Route.bindUnprotectedAccountRoute(backend: Backend, databaseReader: Database } } -fun Route.bindProtectedAccountRoute() { +fun Route.bindProtectedAccountRoute(reader: DatabaseReader) { post { - usePrincipal { _ -> + usePrincipal(reader) { _ -> call.sessions.clear(UserSession::class) Result.success(Unit) } 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 a83cd6c..120e443 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 @@ -1,5 +1,6 @@ package com.storyteller_f.a.server.auth +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.CustomBadRequestException import com.storyteller_f.ForbiddenException import com.storyteller_f.UnauthorizedException @@ -12,11 +13,13 @@ import io.ktor.server.plugins.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.websocket.* -import io.ktor.utils.io.streams.* import java.io.File -suspend inline fun RoutingContext.usePrincipal(block: (PrimaryKey) -> Result) { - usePrincipalOrNull { +suspend inline fun RoutingContext.usePrincipal( + reader: DatabaseReader, + block: (PrimaryKey) -> Result +) { + usePrincipalOrNull(reader) { if (it != null) { block(it) } else { @@ -25,22 +28,23 @@ suspend inline fun RoutingContext.usePrincipal(block: (Primary } } -suspend inline fun RoutingContext.omitPrincipal(block: () -> Result) { - usePrincipalOrNull { +suspend inline fun RoutingContext.omitPrincipal(reader: DatabaseReader, block: () -> Result) { + usePrincipalOrNull(reader) { block() } } -suspend inline fun RoutingContext.usePrincipalOrNull(block: (PrimaryKey?) -> Result) { +suspend inline fun RoutingContext.usePrincipalOrNull( + reader: DatabaseReader, + block: (PrimaryKey?) -> Result +) { val uid = call.principal()?.uid // 有可能存在bug 导致block 抛出异常,所以需要进行一次try catch try { block(uid).onSuccess { when (it) { null -> call.respond(HttpStatusCode.NotFound) - is MediaResponse -> { - call.respondFile(File(it.file)) - } + is MediaResponse -> call.respondFile(File(it.file)) is Unit -> call.respond(HttpStatusCode.OK) else -> call.respond(it) } @@ -48,17 +52,17 @@ suspend inline fun RoutingContext.usePrincipalOrNull(block: (P if (it !is UnauthorizedException) { call.application.log.error("Occur exception", it) } - respondError(it) + respondError(it, reader) } } catch (e: Exception) { call.application.log.error("Catch exception in api", e) } } -suspend fun RoutingContext.respondError(e: Throwable) { +suspend fun RoutingContext.respondError(e: Throwable, reader: DatabaseReader) { when (e) { is ForbiddenException -> call.respond(HttpStatusCode.Forbidden, e.message.toString()) - is UnauthorizedException -> call.respondUnauthorizedResponse() + is UnauthorizedException -> call.respondUnauthorizedResponse(reader) is MissingRequestParameterException, is ParameterConversionException, is ContentTransformationException -> call.respond( HttpStatusCode.BadRequest, @@ -69,6 +73,7 @@ suspend fun RoutingContext.respondError(e: Throwable) { 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/route/Route.kt b/server/src/main/kotlin/com/storyteller_f/a/server/route/Route.kt index dee9af2..2a60845 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/route/Route.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/route/Route.kt @@ -1,5 +1,6 @@ package com.storyteller_f.a.server.route +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.Backend import com.storyteller_f.a.server.auth.* import com.storyteller_f.a.server.common.checkParameter @@ -107,7 +108,7 @@ class RouteTopics(val fillHasCommented: Boolean? = null) { @Resource("reactions") class RouteReactions { @Resource("delete") - class Delete(val parent: RouteReactions) + class Delete(@Suppress("unused") val parent: RouteReactions) } @Resource("/users") @@ -137,37 +138,37 @@ class RouteAccounts { class GetData(@Suppress("unused")val parent: RouteAccounts) } -fun Application.commonRoute(backend: Backend) { +fun Application.commonRoute(backend: Backend, reader: DatabaseReader) { routing { authenticate { - bindProtectedSafeRoomRoute(backend) - bindProtectedSafeTopicRoute(backend) - bindProtectedSafeCommunityRoute(backend) - bindProtectedSafeUserRoute() + bindProtectedSafeRoomRoute(backend, reader) + bindProtectedSafeTopicRoute(backend, reader) + bindProtectedSafeCommunityRoute(backend, reader) + bindProtectedSafeUserRoute(reader) webSocket("/link") { webSocketContent(backend) } - bindProtectedAccountRoute() - bindProtectedSafeMediaRoute(backend) + bindProtectedAccountRoute(reader) + bindProtectedSafeMediaRoute(backend, reader) } authenticate(optional = true) { - bindSafeRoomRoute(backend) - bindSafeTopicRoute(backend) - bindSafeCommunityRoute(backend) - bindSafeUserRoute(backend) + bindSafeRoomRoute(backend, reader) + bindSafeTopicRoute(backend, reader) + bindSafeCommunityRoute(backend, reader) + bindSafeUserRoute(backend, reader) } - bindUnprotectedAccountRoute(backend, ) - bindUnauthenticatedRoute(backend) + bindUnprotectedAccountRoute(backend, reader) + bindUnauthenticatedRoute(backend, reader) } } -fun Routing.bindUnauthenticatedRoute(backend: Backend) { +fun Routing.bindUnauthenticatedRoute(backend: Backend, reader: DatabaseReader) { get("/ping") { call.respondText("pong") } get("/amedia/{path...}") { - omitPrincipal { + omitPrincipal(reader) { checkParameter, MediaResponse>("path") { val service = backend.mediaService if (service is FileSystemMediaService) { diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeCommunityRoute.kt b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeCommunityRoute.kt index 66e0049..fa02bfe 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeCommunityRoute.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeCommunityRoute.kt @@ -1,5 +1,6 @@ package com.storyteller_f.a.server.route +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.Backend import com.storyteller_f.a.server.auth.usePrincipal import com.storyteller_f.a.server.auth.usePrincipalOrNull @@ -10,9 +11,9 @@ import com.storyteller_f.tables.searchMembers import io.ktor.server.resources.* import io.ktor.server.routing.Route -fun Route.bindSafeCommunityRoute(backend: Backend) { +fun Route.bindSafeCommunityRoute(backend: Backend, reader: DatabaseReader) { get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> pagination(PrimaryKey::class, { it.id.toString() }) { p, n, s -> @@ -22,7 +23,7 @@ fun Route.bindSafeCommunityRoute(backend: Backend) { } get { - usePrincipalOrNull { _ -> + usePrincipalOrNull(reader) { _ -> pagination(PrimaryKey::class, { it.id.toString() }) { p, n, s -> @@ -31,26 +32,26 @@ fun Route.bindSafeCommunityRoute(backend: Backend) { } } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> getCommunity(it.id, null, backend, id, it.parent.fillJoinInfo) } } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> getCommunity(null, it.aid, backend, id, it.fillJoinInfo) } } } -fun Route.bindProtectedSafeCommunityRoute(backend: Backend) { +fun Route.bindProtectedSafeCommunityRoute(backend: Backend, reader: DatabaseReader) { post { - usePrincipal { id -> + usePrincipal(reader) { id -> joinCommunity(id, it.parent.id, backend) } } post { - usePrincipal { uid -> + usePrincipal(reader) { uid -> exitCommunity(it.parent.id, uid, backend) } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeMediaRoute.kt b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeMediaRoute.kt index 411d4fd..8556dc1 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeMediaRoute.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeMediaRoute.kt @@ -1,21 +1,18 @@ package com.storyteller_f.a.server.route +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.Backend import com.storyteller_f.a.server.auth.usePrincipal import com.storyteller_f.a.server.service.getMediaList import com.storyteller_f.a.server.service.uploadMedia -import io.ktor.http.content.* -import io.ktor.server.plugins.* -import io.ktor.server.request.* import io.ktor.server.resources.* import io.ktor.server.resources.post import io.ktor.server.routing.* -import io.ktor.utils.io.* import java.io.File -fun Route.bindProtectedSafeMediaRoute(backend: Backend) { +fun Route.bindProtectedSafeMediaRoute(backend: Backend, reader: DatabaseReader) { get { - usePrincipal { id -> + usePrincipal(reader) { id -> getMediaList(id, backend, it) } } @@ -27,7 +24,7 @@ fun Route.bindProtectedSafeMediaRoute(backend: Backend) { } post { - usePrincipal { id -> + usePrincipal(reader) { id -> uploadMedia(it, id, root, backend) } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeRoomRoute.kt b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeRoomRoute.kt index 8b1cbbd..e51523f 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeRoomRoute.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeRoomRoute.kt @@ -1,5 +1,6 @@ package com.storyteller_f.a.server.route +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.Backend import com.storyteller_f.UnauthorizedException import com.storyteller_f.a.server.auth.usePrincipal @@ -14,9 +15,9 @@ import com.storyteller_f.tables.searchRooms import io.ktor.server.resources.* import io.ktor.server.routing.Route -fun Route.bindSafeRoomRoute(backend: Backend) { +fun Route.bindSafeRoomRoute(backend: Backend, reader: DatabaseReader) { get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> pagination(PrimaryKey::class, { it.id.toString() }) { p, n, size -> @@ -26,7 +27,7 @@ fun Route.bindSafeRoomRoute(backend: Backend) { } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> pagination(PrimaryKey::class, { it.id.toString() }) { p, n, s -> @@ -42,7 +43,7 @@ fun Route.bindSafeRoomRoute(backend: Backend) { } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> it.aid?.let { aid -> getRoom(null, aid, id, backend, it.fillJoinInfo) } ?: Result.success(null) @@ -50,13 +51,13 @@ fun Route.bindSafeRoomRoute(backend: Backend) { } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> getRoom(it.id, null, id, backend, it.parent.fillJoinInfo) } } get { - usePrincipalOrNull { uid -> + usePrincipalOrNull(reader) { uid -> pagination(PrimaryKey::class, { it.id.toString() }) { pre, next, size -> @@ -66,14 +67,14 @@ fun Route.bindSafeRoomRoute(backend: Backend) { } } -fun Route.bindProtectedSafeRoomRoute(backend: Backend) { +fun Route.bindProtectedSafeRoomRoute(backend: Backend, reader: DatabaseReader) { post { - usePrincipal { id -> + usePrincipal(reader) { id -> joinRoom(it.parent.id, id, backend) } } get { - usePrincipal { id -> + usePrincipal(reader) { id -> pagination(PrimaryKey::class, { it.first.toString() }) { pre, next, size -> @@ -82,7 +83,7 @@ fun Route.bindProtectedSafeRoomRoute(backend: Backend) { } } post { - usePrincipal { uid -> + usePrincipal(reader) { uid -> exitRoom(it.parent.id, uid, backend) } } diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeTopicRoute.kt b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeTopicRoute.kt index 33d0b53..0cfe89e 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeTopicRoute.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeTopicRoute.kt @@ -1,10 +1,12 @@ package com.storyteller_f.a.server.route +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.Backend import com.storyteller_f.a.server.auth.usePrincipal import com.storyteller_f.a.server.auth.usePrincipalOrNull import com.storyteller_f.a.server.common.pagination import com.storyteller_f.a.server.service.* +import com.storyteller_f.shared.obj.DeleteReaction import com.storyteller_f.shared.obj.NewReaction import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.type.PrimaryKey @@ -15,9 +17,9 @@ import io.ktor.server.request.* import io.ktor.server.resources.* import io.ktor.server.routing.Route -fun Route.bindSafeTopicRoute(backend: Backend) { +fun Route.bindSafeTopicRoute(backend: Backend, reader: DatabaseReader) { get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> pagination(PrimaryKey::class, { it.id.toString() }) { _, n, s -> @@ -27,7 +29,7 @@ fun Route.bindSafeTopicRoute(backend: Backend) { } get { - usePrincipalOrNull { uid -> + usePrincipalOrNull(reader) { uid -> pagination(PrimaryKey::class, { it.id.toString() }) { prePageToken, nextPageToken, size -> @@ -37,13 +39,13 @@ fun Route.bindSafeTopicRoute(backend: Backend) { } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> getTopic(it.id, id, backend, it.parent.fillHasCommented) } } get { - usePrincipalOrNull { uid -> + usePrincipalOrNull(reader) { uid -> pagination(PrimaryKey::class, { it.id.toString() }) { p, n, s -> @@ -53,23 +55,23 @@ fun Route.bindSafeTopicRoute(backend: Backend) { } } -fun Route.bindProtectedSafeTopicRoute(backend: Backend) { +fun Route.bindProtectedSafeTopicRoute(backend: Backend, reader: DatabaseReader) { get { - usePrincipal { id -> + usePrincipal(reader) { id -> createTopicSnapshot(id, it.parent.id, backend) } } post { - usePrincipal { + usePrincipal(reader) { addTopicAtCommunity(it, backend) } } post { - usePrincipal { id -> + usePrincipal(reader) { id -> val emoji = call.receive().emoji - if (EmojiReader.getTextLength(emoji) == 1 && EmojiReader.isEmojiOfCharIndex(emoji, 0)) { + if (isEmoji(emoji)) { addReaction(id, it.parent.id, emoji) } else { Result.failure(BadRequestException("invalid emoji")) @@ -78,10 +80,11 @@ fun Route.bindProtectedSafeTopicRoute(backend: Backend) { } post { - usePrincipal { id -> - val emoji = call.receive().emoji - if (EmojiReader.getTextLength(emoji) == 1 && EmojiReader.isEmojiOfCharIndex(emoji, 0)) { - deleteReaction(id, emoji) + usePrincipal(reader) { id -> + val deleteReaction = call.receive() + val emoji = deleteReaction.emoji + if (isEmoji(emoji)) { + deleteReaction(id, emoji, deleteReaction.objectId) } else { Result.failure(BadRequestException("invalid emoji")) } @@ -89,8 +92,10 @@ fun Route.bindProtectedSafeTopicRoute(backend: Backend) { } get { - usePrincipalOrNull { id -> + usePrincipalOrNull(reader) { id -> reactionList(it.parent.id, id, it.parent.parent.fillHasCommented) } } } + +private fun isEmoji(emoji: String) = EmojiReader.getTextLength(emoji) == 1 && EmojiReader.isEmojiOfCharIndex(emoji, 0) diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeUserRoute.kt b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeUserRoute.kt index e44d72b..b5c0e86 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeUserRoute.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/route/SafeUserRoute.kt @@ -1,5 +1,6 @@ package com.storyteller_f.a.server.route +import com.maxmind.geoip2.DatabaseReader import com.storyteller_f.Backend import com.storyteller_f.a.server.auth.omitPrincipal import com.storyteller_f.a.server.auth.usePrincipal @@ -12,28 +13,28 @@ import com.storyteller_f.tables.searchMembers import io.ktor.server.resources.* import io.ktor.server.routing.Route -fun Route.bindProtectedSafeUserRoute() { +fun Route.bindProtectedSafeUserRoute(reader: DatabaseReader) { post { - usePrincipal { id -> + usePrincipal(reader) { id -> updateUser(id) } } } -fun Route.bindSafeUserRoute(backend: Backend) { +fun Route.bindSafeUserRoute(backend: Backend, reader: DatabaseReader) { get { value -> - omitPrincipal { + omitPrincipal(reader) { value.aid?.let { getUserByAid(it, backend) } ?: Result.success(null) } } get { - omitPrincipal { + omitPrincipal(reader) { getUser(it.id, backend = backend) } } get { - omitPrincipal { + omitPrincipal(reader) { pagination(PrimaryKey::class, { it.id.toString() }) { p, n, s -> diff --git a/server/src/test/kotlin/TopicTest.kt b/server/src/test/kotlin/TopicTest.kt index 12accab..b640a42 100644 --- a/server/src/test/kotlin/TopicTest.kt +++ b/server/src/test/kotlin/TopicTest.kt @@ -75,15 +75,16 @@ class TopicTest { client.addReaction(topicInfo.id, emoji).getOrThrow() topicInfo } + val topicId = session1.data5.id attachSession(client) { client.joinCommunity(newCommunity) - val reactions = client.getReactions(session1.data5.id).getOrThrow() + val reactions = client.getReactions(topicId).getOrThrow() assertEquals(1, reactions.data.size) assertFalse(reactions.data.first().hasReacted) } loginSession(client, session1) { - assertTrue(client.deleteReaction(emoji).getOrThrow()) - assertEquals(0, client.getReactions(session1.data5.id).getOrThrow().data.size) + assertTrue(client.deleteReaction(emoji, topicId).getOrThrow()) + assertEquals(0, client.getReactions(topicId).getOrThrow().data.size) } } } diff --git a/shared/src/commonMain/kotlin/com/storyteller_f/shared/model/TopicInfo.kt b/shared/src/commonMain/kotlin/com/storyteller_f/shared/model/TopicInfo.kt index ab187d7..858648e 100644 --- a/shared/src/commonMain/kotlin/com/storyteller_f/shared/model/TopicInfo.kt +++ b/shared/src/commonMain/kotlin/com/storyteller_f/shared/model/TopicInfo.kt @@ -51,6 +51,10 @@ sealed interface TopicContent { @SerialName("nil") data object Nil : TopicContent + @Serializable + @SerialName("extracted") + data class Extracted(val plain: String, val list: List = emptyList()) : TopicContent + @Serializable @SerialName("plain") data class Plain(val plain: String, val list: List = emptyList()) : TopicContent diff --git a/shared/src/commonMain/kotlin/com/storyteller_f/shared/obj/NewReaction.kt b/shared/src/commonMain/kotlin/com/storyteller_f/shared/obj/NewReaction.kt index 00fa753..b908268 100644 --- a/shared/src/commonMain/kotlin/com/storyteller_f/shared/obj/NewReaction.kt +++ b/shared/src/commonMain/kotlin/com/storyteller_f/shared/obj/NewReaction.kt @@ -1,6 +1,10 @@ package com.storyteller_f.shared.obj +import com.storyteller_f.shared.type.PrimaryKey import kotlinx.serialization.Serializable @Serializable data class NewReaction(val emoji: String) + +@Serializable +data class DeleteReaction(val emoji: String, val objectId: PrimaryKey)