From 5e61c2a770b8053cee9916aa36e385f992d7a8af Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 22 Jan 2022 13:30:49 +0600 Subject: [PATCH] add integration with posts creating --- client/build.gradle | 3 + .../kotlin/dev/inmo/postssystem/client/DI.kt | 9 +- ...naryContentSerializerModuleConfigurator.kt | 11 - .../binary/common/DefaultBinaryContent.kt | 14 -- .../server/BinaryServerContentStorage.kt | 13 +- .../features/content/common/Content.kt | 11 +- gradle.properties | 6 +- server/build.gradle | 2 + .../java/dev/inmo/postssystem/server/DI.kt | 4 +- .../posts/client/ClientPostsService.kt | 11 + .../posts/client/ClientReadPostsService.kt | 22 ++ .../posts/client/ClientWritePostsService.kt | 123 ++++++++++ services/posts/common/build.gradle | 2 + .../services/posts/common/Constants.kt | 2 + .../services/posts/common/FullNewPost.kt | 9 + .../services/posts/common/PostsService.kt | 3 + .../services/posts/common/ReadPostsService.kt | 7 + .../posts/common/WritePostsService.kt | 16 ++ services/posts/server/build.gradle | 2 + .../posts/server/DefaultWritePostsService.kt | 49 ++++ .../posts/server/DownloadFullNewPost.kt | 61 +++++ .../ServerPostsServiceRoutingConfigurator.kt | 211 ++++++++++++++++++ 22 files changed, 548 insertions(+), 43 deletions(-) delete mode 100644 features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/BinaryContentSerializerModuleConfigurator.kt delete mode 100644 features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/DefaultBinaryContent.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientPostsService.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientReadPostsService.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt create mode 100644 services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/FullNewPost.kt create mode 100644 services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/PostsService.kt create mode 100644 services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/ReadPostsService.kt create mode 100644 services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/WritePostsService.kt create mode 100644 services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DefaultWritePostsService.kt create mode 100644 services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DownloadFullNewPost.kt create mode 100644 services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt diff --git a/client/build.gradle b/client/build.gradle index ded10e89..4437d3e0 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -23,6 +23,9 @@ kotlin { api project(":postssystem.features.content.client") api project(":postssystem.features.content.text.client") api project(":postssystem.features.content.binary.client") + + api project(":postssystem.services.posts.client") + api "dev.inmo:micro_utils.fsm.common:$microutils_version" api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version" api "dev.inmo:micro_utils.crypto:$microutils_version" diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt index 909dfff6..733dbd9f 100644 --- a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt @@ -25,11 +25,12 @@ import dev.inmo.postssystem.client.settings.auth.AuthSettings import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings import dev.inmo.postssystem.features.common.common.SerializersModuleConfigurator import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier -import dev.inmo.postssystem.features.content.binary.common.BinaryContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator import dev.inmo.postssystem.features.status.client.StatusFeatureClient +import dev.inmo.postssystem.services.posts.client.ClientPostsService +import dev.inmo.postssystem.services.posts.common.* import io.ktor.client.HttpClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,6 +44,7 @@ import org.koin.core.context.startKoin import org.koin.core.module.Module import org.koin.core.qualifier.* import org.koin.core.scope.Scope +import org.koin.dsl.binds import org.koin.dsl.module val UIScopeQualifier = StringQualifier("CoroutineScopeUI") @@ -69,7 +71,6 @@ fun baseKoin( module { singleWithRandomQualifier { OtherContentSerializerModuleConfigurator } singleWithRandomQualifier { TextContentSerializerModuleConfigurator } - singleWithRandomQualifier { BinaryContentSerializerModuleConfigurator } singleWithRandomQualifier { ContentSerializersModuleConfigurator(getAll()) } single { SerializersModuleConfigurator(getAll()) } @@ -133,5 +134,9 @@ fun getAuthorizedFeaturesDIModule( single { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) } single { UsersStorageKtorClient(get(serverUrlQualifier), get()) } single> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) } + single { ClientPostsService(get(serverUrlQualifier), get()) } binds arrayOf( + ReadPostsService::class, + WritePostsService::class + ) } } diff --git a/features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/BinaryContentSerializerModuleConfigurator.kt b/features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/BinaryContentSerializerModuleConfigurator.kt deleted file mode 100644 index e72de93d..00000000 --- a/features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/BinaryContentSerializerModuleConfigurator.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.inmo.postssystem.features.content.binary.common - -import dev.inmo.postssystem.features.content.common.Content -import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator -import kotlinx.serialization.modules.PolymorphicModuleBuilder - -object BinaryContentSerializerModuleConfigurator : ContentSerializersModuleConfigurator.Element { - override fun PolymorphicModuleBuilder.invoke() { - subclass(DefaultBinaryContent::class, DefaultBinaryContent.serializer()) - } -} diff --git a/features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/DefaultBinaryContent.kt b/features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/DefaultBinaryContent.kt deleted file mode 100644 index 80c808dd..00000000 --- a/features/content/binary/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/binary/common/DefaultBinaryContent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.inmo.postssystem.features.content.binary.common - -import dev.inmo.micro_utils.common.FileName -import dev.inmo.micro_utils.mime_types.MimeType -import dev.inmo.postssystem.features.common.common.SimpleInputProvider -import dev.inmo.postssystem.features.content.common.BinaryContent -import kotlinx.serialization.Serializable - -@Serializable -data class DefaultBinaryContent( - override val filename: FileName, - override val mimeType: MimeType, - override val inputProvider: SimpleInputProvider -) : BinaryContent diff --git a/features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/BinaryServerContentStorage.kt b/features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/BinaryServerContentStorage.kt index 75a163b6..250c37c1 100644 --- a/features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/BinaryServerContentStorage.kt +++ b/features/content/binary/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/content/binary/server/BinaryServerContentStorage.kt @@ -2,7 +2,6 @@ package dev.inmo.postssystem.features.content.binary.server import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.repos.UpdatedValuePair -import dev.inmo.postssystem.features.content.binary.common.DefaultBinaryContent import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage import dev.inmo.postssystem.features.files.common.* @@ -12,7 +11,7 @@ import kotlinx.coroutines.flow.map class BinaryServerContentStorage( private val filesStorage: FilesStorage -) : ServerContentStorage { +) : ServerContentStorage { private val FileId.asContentId get() = ContentId(string) private val ContentId.asFileId @@ -20,13 +19,13 @@ class BinaryServerContentStorage( private val FullFileInfoStorageWrapper.asRegisteredContent get() = RegisteredContent( id.asContentId, - DefaultBinaryContent( + BinaryContent( fileInfo.name, fileInfo.mimeType, fileInfo.inputProvider ) ) - private val DefaultBinaryContent.asFullFileInfo + private val BinaryContent.asFullFileInfo get() = FullFileInfo( filename, mimeType, @@ -36,7 +35,7 @@ class BinaryServerContentStorage( override val newObjectsFlow: Flow = filesStorage.newObjectsFlow.map { it.asRegisteredContent } override val updatedObjectsFlow: Flow = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent } - override suspend fun create(values: List): List { + override suspend fun create(values: List): List { return filesStorage.create( values.map { it.asFullFileInfo } ).map { it.asRegisteredContent } @@ -46,14 +45,14 @@ class BinaryServerContentStorage( filesStorage.deleteById(ids.map { it.asFileId }) } - override suspend fun update(id: ContentId, value: DefaultBinaryContent): RegisteredContent? { + override suspend fun update(id: ContentId, value: BinaryContent): RegisteredContent? { return filesStorage.update( id.asFileId, value.asFullFileInfo ) ?.asRegisteredContent } - override suspend fun update(values: List>): List { + override suspend fun update(values: List>): List { return filesStorage.update( values.map { (id, content) -> id.asFileId to content.asFullFileInfo diff --git a/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt b/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt index a5704ff6..254c83a0 100644 --- a/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt +++ b/features/content/common/src/commonMain/kotlin/dev/inmo/postssystem/features/content/common/Content.kt @@ -3,6 +3,7 @@ package dev.inmo.postssystem.features.content.common import dev.inmo.micro_utils.common.FileName import dev.inmo.micro_utils.mime_types.MimeType import dev.inmo.postssystem.features.common.common.SimpleInputProvider +import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.Serializable import kotlin.jvm.JvmInline @@ -26,11 +27,13 @@ interface SimpleContent : Content /** * This type represents some binary data which can be sent with multipart and deserialized from it */ -interface BinaryContent : Content { - val filename: FileName - val mimeType: MimeType +data class BinaryContent( + val filename: FileName, + val mimeType: MimeType, val inputProvider: SimpleInputProvider -} +) : Content + +val ContentSerializer = PolymorphicSerializer(Content::class) /** * Content which is already registered in database. Using its [id] you can retrieve all known diff --git a/gradle.properties b/gradle.properties index e7bdee86..0f6a79f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,13 +13,13 @@ kotlin_version=1.6.10 kotlin_serialisation_core_version=1.3.2 koin_version=3.1.2 -microutils_version=0.9.1 +microutils_version=0.9.4 ktor_version=1.6.7 logback_version=1.2.10 uuid_version=0.3.1 -klock_version=2.4.10 +klock_version=2.4.12 -tgbotapi_version=0.38.1 +tgbotapi_version=0.38.3 # Server diff --git a/server/build.gradle b/server/build.gradle index f4549406..262538db 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -23,6 +23,8 @@ dependencies { api project(":postssystem.features.content.binary.server") api project(":postssystem.features.publication.server") + api project(":postssystem.services.posts.server") + api project(":postssystem.targets.telegram.publication.server") api "io.ktor:ktor-server-netty:$ktor_version" diff --git a/server/src/main/java/dev/inmo/postssystem/server/DI.kt b/server/src/main/java/dev/inmo/postssystem/server/DI.kt index 2a2265a7..2f14f6b8 100644 --- a/server/src/main/java/dev/inmo/postssystem/server/DI.kt +++ b/server/src/main/java/dev/inmo/postssystem/server/DI.kt @@ -25,7 +25,6 @@ import dev.inmo.micro_utils.ktor.server.createKtorServer import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo import dev.inmo.postssystem.features.common.common.* -import dev.inmo.postssystem.features.content.binary.common.BinaryContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.binary.server.BinaryServerContentStorage import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage @@ -36,6 +35,7 @@ import dev.inmo.postssystem.features.posts.server.ExposedServerPostsStorage import dev.inmo.postssystem.features.posts.server.ServerPostsStorage import dev.inmo.postssystem.features.publication.server.PublicationManager import dev.inmo.postssystem.features.publication.server.PublicationTarget +import dev.inmo.postssystem.services.posts.server.ServerPostsServiceRoutingConfigurator import dev.inmo.postssystem.targets.telegram.publication.server.PublicationTargetTelegram import io.ktor.application.featureOrNull import io.ktor.application.log @@ -86,7 +86,6 @@ fun getDIModule( return module { singleWithRandomQualifier { OtherContentSerializerModuleConfigurator } singleWithRandomQualifier { TextContentSerializerModuleConfigurator } - singleWithRandomQualifier { BinaryContentSerializerModuleConfigurator } singleWithRandomQualifier { ContentSerializersModuleConfigurator(getAll()) } single { SerializersModuleConfigurator(getAll()) } @@ -184,6 +183,7 @@ fun getDIModule( singleWithBinds { UsersStorageServerRoutesConfigurator(get(), get()) } singleWithBinds { RolesStorageReadServerRoutesConfigurator(get(), RoleSerializer, get()) } singleWithBinds { RolesManagerRolesStorageServerRoutesConfigurator(get(), get()) } + singleWithBinds { ServerPostsServiceRoutingConfigurator(get(), get(), get()) } singleWithBinds { ClientStaticRoutingConfiguration(get().clientStatic) } singleWithBinds { diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientPostsService.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientPostsService.kt new file mode 100644 index 00000000..fe221187 --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientPostsService.kt @@ -0,0 +1,11 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.postssystem.services.posts.common.* + +class ClientPostsService( + baseUrl: String, + unifiedRequester: UnifiedRequester +) : PostsService, + ReadPostsService by ClientReadPostsService(baseUrl, unifiedRequester), + WritePostsService by ClientWritePostsService(baseUrl, unifiedRequester) diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientReadPostsService.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientReadPostsService.kt new file mode 100644 index 00000000..94a7e53e --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientReadPostsService.kt @@ -0,0 +1,22 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import dev.inmo.micro_utils.repos.ReadCRUDRepo +import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo +import dev.inmo.postssystem.features.posts.common.PostId +import dev.inmo.postssystem.features.posts.common.RegisteredPost +import dev.inmo.postssystem.services.posts.common.ReadPostsService +import dev.inmo.postssystem.services.posts.common.postsRootPath +import kotlinx.serialization.builtins.nullable + +class ClientReadPostsService( + private val baseUrl: String, + private val unifiedRequester: UnifiedRequester +) : ReadPostsService, ReadCRUDRepo by KtorReadStandardCrudRepo( + buildStandardUrl(baseUrl, postsRootPath), + unifiedRequester, + RegisteredPost.serializer(), + RegisteredPost.serializer().nullable, + PostId.serializer() +) diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt new file mode 100644 index 00000000..8e9ce037 --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt @@ -0,0 +1,123 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import dev.inmo.micro_utils.ktor.common.encodeHex +import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting +import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting +import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute +import dev.inmo.postssystem.features.content.common.* +import dev.inmo.postssystem.features.posts.common.PostId +import dev.inmo.postssystem.features.posts.common.RegisteredPost +import dev.inmo.postssystem.services.posts.common.* +import io.ktor.client.request.forms.InputProvider +import io.ktor.client.request.forms.formData +import io.ktor.client.request.headers +import io.ktor.client.request.post +import io.ktor.http.HttpHeaders +import kotlinx.serialization.builtins.* + +class ClientWritePostsService( + private val baseUrl: String, + private val unifiedRequester: UnifiedRequester +) : WritePostsService { + private val root = buildStandardUrl(baseUrl, postsRootPath) + + private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer) + private val contentsEitherSerializer = ListSerializer(contentEitherSerializer) + private val contentsSerializer = ListSerializer(ContentSerializer) + + private val createFullPath = buildStandardUrl( + root, + createRouting + ) + private val removeFullPath = buildStandardUrl( + root, + removeRoute + ) + + override suspend fun create(newPost: FullNewPost): RegisteredPost? { + return if (newPost.content.any { it is BinaryContent }) { + val answer = unifiedRequester.client.post(createFullPath) { + formData { + newPost.content.forEachIndexed { i, content -> + when (content) { + is BinaryContent -> append( + i.toString(), + InputProvider(block = content.inputProvider), + headers { + append(HttpHeaders.ContentType, content.mimeType.raw) + append(HttpHeaders.ContentDisposition, "filename=\"${content.filename.name}\"") + }.build() + ) + else -> append( + i.toString(), + unifiedRequester.serialFormat.encodeHex(ContentSerializer, content) + ) + } + } + } + } + unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer) + } else { + unifiedRequester.unipost( + createFullPath, + contentsSerializer to newPost.content, + RegisteredPost.serializer().nullable + ) + } + } + + override suspend fun update( + postId: PostId, + content: List> + ): RegisteredPost? { + return if (content.any { it.optionalT2.data is BinaryContent }) { + val answer = unifiedRequester.client.post(createFullPath) { + formData { + content.forEachIndexed { i, eitherContent -> + eitherContent.onFirst { + append( + i.toString(), + unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either()) + ) + }.onSecond { + when (it) { + is BinaryContent -> append( + i.toString(), + InputProvider(block = it.inputProvider), + headers { + append(HttpHeaders.ContentType, it.mimeType.raw) + append(HttpHeaders.ContentDisposition, "filename=\"${it.filename.name}\"") + }.build() + ) + else -> append( + i.toString(), + unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either()) + ) + } + } + } + } + } + unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer) + } else { + unifiedRequester.unipost( + buildStandardUrl( + root, + updateRouting, + postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId) + ), + contentsEitherSerializer to content, + RegisteredPost.serializer().nullable + ) + } + } + + override suspend fun remove(postId: PostId) = unifiedRequester.unipost( + removeFullPath, + PostId.serializer() to postId, + Unit.serializer() + ) +} diff --git a/services/posts/common/build.gradle b/services/posts/common/build.gradle index 71c2c7e9..5ec15af9 100644 --- a/services/posts/common/build.gradle +++ b/services/posts/common/build.gradle @@ -12,6 +12,8 @@ kotlin { dependencies { api project(":postssystem.features.common.common") api project(":postssystem.features.posts.common") + api "dev.inmo:micro_utils.repos.common:$microutils_version" + api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version" } } } diff --git a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt index 4483418a..a30c0e0c 100644 --- a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt +++ b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt @@ -1,3 +1,5 @@ package dev.inmo.postssystem.services.posts.common const val postsRootPath = "posts" + +const val postsPostIdParameter = "postId" diff --git a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/FullNewPost.kt b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/FullNewPost.kt new file mode 100644 index 00000000..3aa9de3a --- /dev/null +++ b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/FullNewPost.kt @@ -0,0 +1,9 @@ +package dev.inmo.postssystem.services.posts.common + +import dev.inmo.postssystem.features.content.common.Content +import kotlinx.serialization.Serializable + +@Serializable +data class FullNewPost( + val content: List +) diff --git a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/PostsService.kt b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/PostsService.kt new file mode 100644 index 00000000..a21a1b3e --- /dev/null +++ b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/PostsService.kt @@ -0,0 +1,3 @@ +package dev.inmo.postssystem.services.posts.common + +interface PostsService : ReadPostsService, WritePostsService diff --git a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/ReadPostsService.kt b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/ReadPostsService.kt new file mode 100644 index 00000000..85969920 --- /dev/null +++ b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/ReadPostsService.kt @@ -0,0 +1,7 @@ +package dev.inmo.postssystem.services.posts.common + +import dev.inmo.micro_utils.repos.ReadCRUDRepo +import dev.inmo.postssystem.features.posts.common.PostId +import dev.inmo.postssystem.features.posts.common.RegisteredPost + +interface ReadPostsService : ReadCRUDRepo diff --git a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/WritePostsService.kt b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/WritePostsService.kt new file mode 100644 index 00000000..115fe3ca --- /dev/null +++ b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/WritePostsService.kt @@ -0,0 +1,16 @@ +package dev.inmo.postssystem.services.posts.common + +import dev.inmo.micro_utils.common.Either +import dev.inmo.postssystem.features.content.common.Content +import dev.inmo.postssystem.features.content.common.ContentId +import dev.inmo.postssystem.features.posts.common.PostId +import dev.inmo.postssystem.features.posts.common.RegisteredPost + +interface WritePostsService { + suspend fun create(newPost: FullNewPost): RegisteredPost? + suspend fun update( + postId: PostId, + content: List> + ): RegisteredPost? + suspend fun remove(postId: PostId) +} diff --git a/services/posts/server/build.gradle b/services/posts/server/build.gradle index 5b0bc9a0..e677259a 100644 --- a/services/posts/server/build.gradle +++ b/services/posts/server/build.gradle @@ -14,6 +14,8 @@ kotlin { api project(":postssystem.features.content.server") api project(":postssystem.features.posts.server") api project(":postssystem.features.users.server") + api "dev.inmo:micro_utils.common:$microutils_version" + api "org.jetbrains.kotlinx:kotlinx-serialization-properties:$kotlin_serialisation_core_version" } } } diff --git a/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DefaultWritePostsService.kt b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DefaultWritePostsService.kt new file mode 100644 index 00000000..d6993b9e --- /dev/null +++ b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DefaultWritePostsService.kt @@ -0,0 +1,49 @@ +package dev.inmo.postssystem.services.posts.server + +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.repos.create +import dev.inmo.micro_utils.repos.deleteById +import dev.inmo.postssystem.features.content.common.Content +import dev.inmo.postssystem.features.content.common.ContentId +import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage +import dev.inmo.postssystem.features.posts.common.* +import dev.inmo.postssystem.features.posts.server.ServerPostsStorage +import dev.inmo.postssystem.services.posts.common.FullNewPost +import dev.inmo.postssystem.services.posts.common.WritePostsService + +class DefaultWritePostsService( + private val postsStorage: ServerPostsStorage, + private val contentStorage: ServerContentStorage +) : WritePostsService { + override suspend fun create(newPost: FullNewPost): RegisteredPost? { + val contentIds = contentStorage.create(newPost.content).map { it.id } + + return postsStorage.create(NewPost(contentIds)).firstOrNull() + } + + override suspend fun update(postId: PostId, content: List>): RegisteredPost? { + if (!postsStorage.contains(postId)) { + return null + } + + val newContent = content.mapNotNull { + when (it) { + is EitherFirst -> { + it.t1 + } + is EitherSecond -> { + contentStorage.create(it.t2).firstOrNull() ?.id + } + } + } + + return postsStorage.update( + postId, + NewPost(newContent) + ) + } + + override suspend fun remove(postId: PostId) { + return postsStorage.deleteById(postId) + } +} diff --git a/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DownloadFullNewPost.kt b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DownloadFullNewPost.kt new file mode 100644 index 00000000..3cc5932c --- /dev/null +++ b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/DownloadFullNewPost.kt @@ -0,0 +1,61 @@ +package dev.inmo.postssystem.services.posts.server + +import dev.inmo.micro_utils.common.FileName +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat +import dev.inmo.micro_utils.ktor.common.decodeHex +import dev.inmo.postssystem.features.content.common.ContentSerializer +import dev.inmo.postssystem.services.posts.common.FullNewPost +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.http.content.PartData +import io.ktor.request.receiveMultipart +import io.ktor.util.asStream +import io.ktor.util.pipeline.PipelineContext +import io.ktor.utils.io.core.use + +suspend fun PipelineContext.downloadFullNewPost( + serialFormat: StandardKtorSerialFormat +): FullNewPost { + val multipart = call.receiveMultipart() + val map = mutableMapOf() + + var part = multipart.readPart() + + while (part != null) { + val name = part.name + val capturedPart = part + when { + name == null -> {} + capturedPart is PartData.FormItem -> { + map[name] = serialFormat.decodeHex( + ContentSerializer, + capturedPart.value + ) + } + capturedPart is PartData.FileItem -> { + val filename = FileName(capturedPart.originalFileName ?: error("File name is unknown for default part")) + val resultInput = MPPFile.createTempFile( + filename.nameWithoutExtension.let { + var resultName = it + while (resultName.length < 3) { + resultName += "_" + } + resultName + }, + ".${filename.extension}" + ).apply { + outputStream().use { fileStream -> + capturedPart.provider().asStream().copyTo(fileStream) + } + } + + } + else -> {} + } + + part = multipart.readPart() + } + + +} diff --git a/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt new file mode 100644 index 00000000..c8500bd2 --- /dev/null +++ b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt @@ -0,0 +1,211 @@ +package dev.inmo.postssystem.services.posts.server + +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.ktor.common.decodeHex +import dev.inmo.micro_utils.ktor.server.* +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import dev.inmo.micro_utils.mime_types.findBuiltinMimeType +import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting +import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting +import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute +import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes +import dev.inmo.postssystem.features.content.common.* +import dev.inmo.postssystem.features.posts.common.* +import dev.inmo.postssystem.services.posts.common.* +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.auth.authenticate +import io.ktor.http.content.PartData +import io.ktor.request.isMultipart +import io.ktor.request.receiveMultipart +import io.ktor.routing.* +import io.ktor.util.asStream +import io.ktor.util.pipeline.PipelineContext +import io.ktor.utils.io.core.use +import io.ktor.utils.io.streams.asInput +import kotlinx.serialization.builtins.* + +class ServerPostsServiceRoutingConfigurator( + private val readPostsService: ReadPostsService, + private val writePostsService: WritePostsService? = readPostsService as? WritePostsService, + private val unifiedRouter: UnifiedRouter +) : ApplicationRoutingConfigurator.Element { + private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer) + private val contentsEitherSerializer = ListSerializer(contentEitherSerializer) + private val contentsSerializer = ListSerializer(ContentSerializer) + + private suspend fun PipelineContext.receiveContents(): List { + return unifiedRouter.run { + if (call.request.isMultipart()) { + val multipart = call.receiveMultipart() + val list = mutableListOf>() + + var part = multipart.readPart() + + while (part != null) { + val name = part.name + val capturedPart = part + when { + name == null -> {} + capturedPart is PartData.FormItem -> { + list.add( + name to unifiedRouter.serialFormat.decodeHex( + ContentSerializer, + capturedPart.value + ) + ) + } + capturedPart is PartData.FileItem -> { + val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part") + val mimeType = capturedPart.contentType ?.let { + findBuiltinMimeType("${it.contentType}/${it.contentSubtype}") + } ?: error("File type is unknown for default part") + val resultInput = MPPFile.createTempFile( + filename.nameWithoutExtension.let { + var resultName = it + while (resultName.length < 3) { + resultName += "_" + } + resultName + }, + ".${filename.extension}" + ).apply { + outputStream().use { fileStream -> + capturedPart.provider().asStream().copyTo(fileStream) + } + } + + list.add( + name to BinaryContent( + filename, + mimeType + ) { resultInput.inputStream().asInput() } + ) + } + else -> {} + } + + part = multipart.readPart() + } + + list.sortedBy { it.first }.map { it.second } + } else { + uniload(contentsSerializer) + } + } + } + + private suspend fun PipelineContext.receiveContentsEithers(): List> { + return unifiedRouter.run { + if (call.request.isMultipart()) { + val multipart = call.receiveMultipart() + val list = mutableListOf>>() + + var part = multipart.readPart() + + while (part != null) { + val name = part.name + val capturedPart = part + when { + name == null -> {} + capturedPart is PartData.FormItem -> { + list.add( + name to unifiedRouter.serialFormat.decodeHex( + contentEitherSerializer, + capturedPart.value + ) + ) + } + capturedPart is PartData.FileItem -> { + val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part") + val mimeType = capturedPart.contentType ?.let { + findBuiltinMimeType("${it.contentType}/${it.contentSubtype}") + } ?: error("File type is unknown for default part") + val resultInput = MPPFile.createTempFile( + filename.nameWithoutExtension.let { + var resultName = it + while (resultName.length < 3) { + resultName += "_" + } + resultName + }, + ".${filename.extension}" + ).apply { + outputStream().use { fileStream -> + capturedPart.provider().asStream().copyTo(fileStream) + } + } + + list.add( + name to BinaryContent( + filename, + mimeType + ) { resultInput.inputStream().asInput() }.either() + ) + } + else -> {} + } + + part = multipart.readPart() + } + + list.sortedBy { it.first }.map { it.second } + } else { + uniload(contentsEitherSerializer) + } + } + } + + override fun Route.invoke() { + authenticate { + route(postsRootPath) { + configureReadStandardCrudRepoRoutes( + readPostsService, + RegisteredPost.serializer(), + RegisteredPost.serializer().nullable, + PostId.serializer(), + unifiedRouter + ) + + writePostsService ?.let { + + unifiedRouter.apply { + + post(createRouting) { + val data = receiveContents() + + unianswer( + RegisteredPost.serializer().nullable, + writePostsService.create(FullNewPost(data)) + ) + } + + post(updateRouting) { + val postId = call.decodeUrlQueryValueOrSendError(postsPostIdParameter, PostId.serializer()) ?: return@post + val data = receiveContentsEithers() + + unianswer( + RegisteredPost.serializer().nullable, + writePostsService.update( + postId, + data + ) + ) + } + + post(removeRoute) { + val postId = uniload(PostId.serializer()) + + unianswer( + Unit.serializer(), + writePostsService.remove(postId) + ) + } + + } + + } + } + } + } +}