All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.org.jraf.klibnotion.internal.client.NotionClientImpl.kt Maven / Gradle / Ivy

There is a newer version: 1.12.0
Show newest version
/*
 * This source is part of the
 *      _____  ___   ____
 *  __ / / _ \/ _ | / __/___  _______ _
 * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
 * \___/_/|_/_/ |_/_/ (_)___/_/  \_, /
 *                              /___/
 * repository.
 *
 * Copyright (C) 2021-present Benoit 'BoD' Lubek ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jraf.klibnotion.internal.client

import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.ProxyBuilder
import io.ktor.client.features.ClientRequestException
import io.ktor.client.features.HttpResponseValidator
import io.ktor.client.features.HttpTimeout
import io.ktor.client.features.UserAgent
import io.ktor.client.features.defaultRequest
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.DEFAULT
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logger
import io.ktor.client.features.logging.Logging
import io.ktor.client.request.header
import io.ktor.client.statement.readText
import io.ktor.http.URLBuilder
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.serialization.json.Json
import org.jraf.klibnotion.client.ClientConfiguration
import org.jraf.klibnotion.client.HttpLoggingLevel
import org.jraf.klibnotion.client.NotionClient
import org.jraf.klibnotion.internal.api.model.apiToModel
import org.jraf.klibnotion.internal.api.model.block.ApiAppendBlocksParametersConverter
import org.jraf.klibnotion.internal.api.model.block.ApiPageResultBlockConverter
import org.jraf.klibnotion.internal.api.model.database.ApiDatabaseConverter
import org.jraf.klibnotion.internal.api.model.database.query.ApiDatabaseQueryConverter
import org.jraf.klibnotion.internal.api.model.modelToApi
import org.jraf.klibnotion.internal.api.model.page.ApiCreateTableParametersConverter
import org.jraf.klibnotion.internal.api.model.page.ApiPageConverter
import org.jraf.klibnotion.internal.api.model.page.ApiPageResultDatabaseConverter
import org.jraf.klibnotion.internal.api.model.page.ApiPageResultPageConverter
import org.jraf.klibnotion.internal.api.model.page.ApiUpdateTableParametersConverter
import org.jraf.klibnotion.internal.api.model.user.ApiUserConverter
import org.jraf.klibnotion.internal.api.model.user.ApiUserResultPageConverter
import org.jraf.klibnotion.internal.klibNotionScope
import org.jraf.klibnotion.internal.model.block.MutableBlock
import org.jraf.klibnotion.model.base.UuidString
import org.jraf.klibnotion.model.block.Block
import org.jraf.klibnotion.model.block.BlockListProducer
import org.jraf.klibnotion.model.block.MutableBlockList
import org.jraf.klibnotion.model.block.invoke
import org.jraf.klibnotion.model.database.Database
import org.jraf.klibnotion.model.database.query.DatabaseQuery
import org.jraf.klibnotion.model.database.query.DatabaseQuerySort
import org.jraf.klibnotion.model.exceptions.NotionClientException
import org.jraf.klibnotion.model.exceptions.NotionClientRequestException
import org.jraf.klibnotion.model.page.Page
import org.jraf.klibnotion.model.pagination.Pagination
import org.jraf.klibnotion.model.pagination.ResultPage
import org.jraf.klibnotion.model.property.value.PropertyValueList
import org.jraf.klibnotion.model.user.User
import kotlin.coroutines.coroutineContext

internal class NotionClientImpl(
    clientConfiguration: ClientConfiguration,
) : NotionClient,
    NotionClient.Users,
    NotionClient.Databases,
    NotionClient.Pages,
    NotionClient.Blocks {

    override val users = this
    override val databases = this
    override val pages = this
    override val blocks = this

    @OptIn(KtorExperimentalAPI::class)
    private val httpClient by lazy {
        createHttpClient(clientConfiguration.httpConfiguration.bypassSslChecks) {
            install(JsonFeature) {
                serializer = KotlinxSerializer(
                    Json {
                        // XXX Comment this to have API changes make the parsing fail
                        ignoreUnknownKeys = true

                        // This is needed to accept JSON Numbers to be deserialized as Strings
                        isLenient = true
                    }
                )
            }
            defaultRequest {
                header(
                    "Authorization",
                    "Bearer ${clientConfiguration.authentication.apiToken}"
                )
            }
            install(UserAgent) {
                agent = clientConfiguration.userAgent
            }
            // Notion API is very slow, so...
            install(HttpTimeout) {
                requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
                connectTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
                socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
            }
            engine {
                // Setup a proxy if requested
                clientConfiguration.httpConfiguration.httpProxy?.let { httpProxy ->
                    proxy = ProxyBuilder.http(URLBuilder().apply {
                        host = httpProxy.host
                        port = httpProxy.port
                    }.build())
                }
            }
            // Setup logging if requested
            if (clientConfiguration.httpConfiguration.loggingLevel != HttpLoggingLevel.NONE) {
                install(Logging) {
                    logger = Logger.DEFAULT
                    level = when (clientConfiguration.httpConfiguration.loggingLevel) {
                        HttpLoggingLevel.NONE -> LogLevel.NONE
                        HttpLoggingLevel.INFO -> LogLevel.INFO
                        HttpLoggingLevel.HEADERS -> LogLevel.HEADERS
                        HttpLoggingLevel.BODY -> LogLevel.BODY
                        HttpLoggingLevel.ALL -> LogLevel.ALL
                    }
                }
            }
            HttpResponseValidator {
                handleResponseException { cause: Throwable ->
                    if (cause is ClientRequestException) throw NotionClientRequestException(
                        cause,
                        cause.response.readText()
                    )
                    throw NotionClientException(cause)
                }
            }
        }
    }

    private val service: NotionService by lazy {
        NotionService(httpClient)
    }

    // region Users

    override suspend fun getUser(id: UuidString): User {
        return service.getUser(id)
            .apiToModel(ApiUserConverter)
    }

    override suspend fun getUserList(pagination: Pagination): ResultPage {
        return service.getUserList(pagination.startCursor)
            .apiToModel(ApiUserResultPageConverter)
    }

    // endregion


    // region Databases

    override suspend fun getDatabase(id: UuidString): Database {
        return service.getDatabase(id)
            .apiToModel(ApiDatabaseConverter)
    }

    override suspend fun getDatabaseList(pagination: Pagination): ResultPage {
        return service.getDatabaseList(pagination.startCursor)
            .apiToModel(ApiPageResultDatabaseConverter)
    }

    override suspend fun queryDatabase(
        id: UuidString,
        query: DatabaseQuery?,
        sort: DatabaseQuerySort?,
        pagination: Pagination,
    ): ResultPage {
        return service.queryDatabase(
            id,
            (query to sort).modelToApi(ApiDatabaseQueryConverter),
            pagination.startCursor
        )
            .apiToModel(ApiPageResultPageConverter)
    }

    // endregion


    // region Pages

    override suspend fun getPage(id: UuidString): Page {
        return service.getPage(id)
            .apiToModel(ApiPageConverter)
    }

    override suspend fun createPage(
        parentDatabaseId: UuidString,
        properties: PropertyValueList,
        content: MutableBlockList?,
    ): Page {
        return service.createPage(
            Triple(
                parentDatabaseId,
                properties.propertyValueList,
                content
            ).modelToApi(ApiCreateTableParametersConverter)
        )
            .apiToModel(ApiPageConverter)
    }

    override suspend fun createPage(
        parentDatabaseId: UuidString,
        properties: PropertyValueList,
        content: BlockListProducer,
    ): Page = createPage(parentDatabaseId, properties, content())

    override suspend fun updatePage(id: UuidString, properties: PropertyValueList): Page {
        return service.updatePage(id, properties.propertyValueList.modelToApi(ApiUpdateTableParametersConverter))
            .apiToModel(ApiPageConverter)
    }

    // endregion


    // region Blocks

    override suspend fun getBlockList(parentId: UuidString, pagination: Pagination): ResultPage {
        val blockResultPage = service.getBlockList(parentId, pagination.startCursor)
            .apiToModel(ApiPageResultBlockConverter)
        getChildrenRecursively(blockResultPage)
        return blockResultPage
    }

    private suspend fun getChildrenRecursively(blockResultPage: ResultPage) {
        val job = Job()
        for (block in blockResultPage.results) {
            if (block is MutableBlock && block.children?.isEmpty() == true) {
                @Suppress("DeferredResultUnused")
                klibNotionScope.async(coroutineContext + job) {
                    val childrenResultPage = getBlockList(block.id)
                    block.children = childrenResultPage.results
                }
            }
        }
        job.children.forEach { it.join() }
    }

    override suspend fun appendBlockList(parentId: UuidString, blocks: MutableBlockList) {
        service.appendBlockList(parentId, blocks.modelToApi(ApiAppendBlocksParametersConverter))
    }

    override suspend fun appendBlockList(parentId: UuidString, blocks: BlockListProducer) =
        appendBlockList(parentId, blocks() ?: MutableBlockList())

    // endregion


    override fun close() = httpClient.close()
}

internal expect fun createHttpClient(
    bypassSslChecks: Boolean,
    block: HttpClientConfig<*>.() -> Unit,
): HttpClient





© 2015 - 2024 Weber Informatics LLC | Privacy Policy