From 5e61c2a770b8053cee9916aa36e385f992d7a8af Mon Sep 17 00:00:00 2001
From: InsanusMokrassar <ovsyannikov.alexey95@gmail.com>
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<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
             singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
-            singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { BinaryContentSerializerModuleConfigurator }
             singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
             single { SerializersModuleConfigurator(getAll()) }
 
@@ -133,5 +134,9 @@ fun getAuthorizedFeaturesDIModule(
         single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) }
         single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
         single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) }
+        single<PostsService> { 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<Content>.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<DefaultBinaryContent> {
+) : ServerContentStorage<BinaryContent> {
     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<RegisteredContent> = filesStorage.newObjectsFlow.map { it.asRegisteredContent }
     override val updatedObjectsFlow: Flow<RegisteredContent> = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent }
 
-    override suspend fun create(values: List<DefaultBinaryContent>): List<RegisteredContent> {
+    override suspend fun create(values: List<BinaryContent>): List<RegisteredContent> {
         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<UpdatedValuePair<ContentId, DefaultBinaryContent>>): List<RegisteredContent> {
+    override suspend fun update(values: List<UpdatedValuePair<ContentId, BinaryContent>>): List<RegisteredContent> {
         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<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
         singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
-        singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { BinaryContentSerializerModuleConfigurator }
         singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
         single { SerializersModuleConfigurator(getAll()) }
 
@@ -184,6 +183,7 @@ fun getDIModule(
         singleWithBinds { UsersStorageServerRoutesConfigurator(get(), get()) }
         singleWithBinds { RolesStorageReadServerRoutesConfigurator<Role>(get(), RoleSerializer, get()) }
         singleWithBinds { RolesManagerRolesStorageServerRoutesConfigurator(get(), get()) }
+        singleWithBinds { ServerPostsServiceRoutingConfigurator(get(), get(), get()) }
 
         singleWithBinds { ClientStaticRoutingConfiguration(get<Config>().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<RegisteredPost, PostId> 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<ByteArray>(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<Either<ContentId, Content>>
+    ): RegisteredPost? {
+        return if (content.any { it.optionalT2.data is BinaryContent }) {
+            val answer = unifiedRequester.client.post<ByteArray>(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<Content>
+)
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<RegisteredPost, PostId>
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<Either<ContentId, Content>>
+    ): 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<Content>
+) : 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<Either<ContentId, Content>>): 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<Unit, ApplicationCall>.downloadFullNewPost(
+    serialFormat: StandardKtorSerialFormat
+): FullNewPost {
+    val multipart = call.receiveMultipart()
+    val map = mutableMapOf<String, Any>()
+
+    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<Unit, ApplicationCall>.receiveContents(): List<Content> {
+        return unifiedRouter.run {
+            if (call.request.isMultipart()) {
+                val multipart = call.receiveMultipart()
+                val list = mutableListOf<Pair<String, Content>>()
+
+                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<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
+        return unifiedRouter.run {
+            if (call.request.isMultipart()) {
+                val multipart = call.receiveMultipart()
+                val list = mutableListOf<Pair<String, Either<ContentId, Content>>>()
+
+                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)
+                            )
+                        }
+
+                    }
+
+                }
+            }
+        }
+    }
+}