Skip to content
This repository has been archived by the owner on Nov 3, 2022. It is now read-only.

Commit

Permalink
Refactor, apply explicit API mode, enhance the token logic
Browse files Browse the repository at this point in the history
  • Loading branch information
petersamokhin committed Mar 2, 2021
1 parent 9426fb8 commit 614326e
Show file tree
Hide file tree
Showing 24 changed files with 546 additions and 404 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# KNotion API
# Notion SDK / Kotlin / Unofficial API

Unofficial Notion.so API wrapper, written in Kotlin. Can be used in Java.
Unofficial Notion API wrapper (SDK), written in Kotlin. Can be used in Java.

### Install

Expand Down
17 changes: 15 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ plugins {
id("maven")
}

kotlin.sourceSets {
all {
allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all {
JavaVersion.VERSION_1_8.toString().also {
kotlinOptions.jvmTarget = it
if (plugins.hasPlugin("org.jetbrains.kotlin.jvm")) {
sourceCompatibility = it
targetCompatibility = it
}
}
}
}

kotlin {
explicitApiWarning()
sourceSets.all {
languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn")
}
}
Expand Down
93 changes: 36 additions & 57 deletions src/main/kotlin/com/petersamokhin/notionapi/Notion.kt
Original file line number Diff line number Diff line change
@@ -1,91 +1,70 @@
package com.petersamokhin.notionapi

import com.petersamokhin.notionapi.mapper.mapTable
import com.petersamokhin.notionapi.model.NotionCredentials
import com.petersamokhin.notionapi.model.NotionTable
import com.petersamokhin.notionapi.model.error.NotionAuthException
import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody
import com.petersamokhin.notionapi.model.request.Loader
import com.petersamokhin.notionapi.model.request.QueryCollectionRequestBody
import com.petersamokhin.notionapi.model.response.NotionResponse
import com.petersamokhin.notionapi.request.LoadPageChunkRequest
import com.petersamokhin.notionapi.request.QueryNotionCollectionRequest
import com.petersamokhin.notionapi.request.base.NotionRequest
import com.petersamokhin.notionapi.utils.dashifyId
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import kotlinx.serialization.json.Json

class Notion internal constructor(token: String, private var httpClient: HttpClient) {
init {
httpClient = httpClient.config {
defaultRequest {
header(HttpHeaders.Cookie, "$NOTION_TOKEN_COOKIE_KEY=$token")
}
}
}
public interface Notion {
public val token: String

suspend fun getCollection(json: Json, pageId: String, sortColumns: Boolean = false): NotionTable? {
val normalPageId = pageId.dashifyId()
val page = loadPage(normalPageId)
public suspend fun getCollection(
json: Json,
pageId: String,
sortColumns: Boolean = false
): NotionTable?

val collectionId = page.recordMap.collectionsMap?.keys?.firstOrNull() ?: return null
val collectionViewId = page.recordMap.collectionViewsMap?.keys?.firstOrNull() ?: return null
public suspend fun loadPage(
pageId: String,
limit: Int = 50
): NotionResponse

val collectionResponse = queryCollection(collectionId, collectionViewId)
public suspend fun queryCollection(
collectionId: String,
collectionViewId: String,
limit: Int = 70
): NotionResponse

return collectionResponse.mapTable(json, sortColumns = sortColumns)
}
public fun setHttpClient(newHttpClient: HttpClient)

suspend fun loadPage(pageId: String, limit: Int = 50): NotionResponse {
return LoadPageChunkRequest(httpClient).execute(
LoadPageChunkRequestBody(pageId, limit, 0, false)
)
}
public fun setToken(token: String)

suspend fun queryCollection(collectionId: String, collectionViewId: String, limit: Int = 70): NotionResponse {
return QueryNotionCollectionRequest(httpClient).execute(
QueryCollectionRequestBody(
collectionId, collectionViewId, Loader(limit, false, "table")
)
)
}
public fun close()

fun close() = httpClient.close()

fun setHttpClient(newHttpClient: HttpClient) {
httpClient = newHttpClient
}

companion object {
private const val NOTION_TOKEN_COOKIE_KEY = "token_v2"
public companion object {
public const val TOKEN_COOKIE_KEY: String = "token_v2"

@JvmStatic
fun fromToken(token: String, httpClient: HttpClient): Notion {
return Notion(token, httpClient)
}
public fun fromToken(token: String, httpClient: HttpClient): Notion =
NotionImpl(token, httpClient)

@OptIn(KtorExperimentalAPI::class)
@JvmStatic
suspend fun fromEmailAndPassword(credentials: NotionCredentials, httpClient: HttpClient): Notion {
public suspend fun fromEmailAndPassword(credentials: NotionCredentials, httpClient: HttpClient): Notion {
val endpoint = "${NotionRequest.API_BASE_URL}/${NotionRequest.Endpoint.LOGIN_WITH_EMAIL}"
val response = httpClient.post<HttpResponse>(endpoint) {
headers.appendAll(NotionRequest.BASE_HEADERS)
contentType(ContentType.Application.Json)
body = credentials
}

println(response)

val token = response.headers.getAll(HttpHeaders.SetCookie)?.firstOrNull {
it.contains("$NOTION_TOKEN_COOKIE_KEY=", true)
}?.split("; ")?.firstOrNull {
it.contains("$NOTION_TOKEN_COOKIE_KEY=", true)
}?.split("=")?.getOrNull(1) ?: throw NotionAuthException("No $NOTION_TOKEN_COOKIE_KEY in headers!")
val token = response.headers.getAll(HttpHeaders.SetCookie)
?.asSequence()
?.map(::parseServerSetCookieHeader)
?.find { it.name == TOKEN_COOKIE_KEY }
?.value
.orEmpty()

return fromToken(token, httpClient)
return fromToken(
token = token,
httpClient = httpClient
)
}
}
}
77 changes: 77 additions & 0 deletions src/main/kotlin/com/petersamokhin/notionapi/NotionImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.petersamokhin.notionapi

import com.petersamokhin.notionapi.mapper.mapTable
import com.petersamokhin.notionapi.model.NotionTable
import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody
import com.petersamokhin.notionapi.model.request.Loader
import com.petersamokhin.notionapi.model.request.QueryCollectionRequestBody
import com.petersamokhin.notionapi.model.response.NotionResponse
import com.petersamokhin.notionapi.request.LoadPageChunkRequest
import com.petersamokhin.notionapi.request.QueryNotionCollectionRequest
import com.petersamokhin.notionapi.utils.dashifyId
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.json.Json
import java.util.concurrent.atomic.AtomicReference

internal class NotionImpl internal constructor(
token: String,
httpClient: HttpClient
) : Notion {
private val _token = AtomicReference(token)

private var _httpClient: HttpClient =
httpClient.withToken()

override val token: String
get() = _token.get()

override suspend fun getCollection(json: Json, pageId: String, sortColumns: Boolean): NotionTable? {
val normalPageId = pageId.dashifyId()
val page = loadPage(normalPageId)

val collectionId = page.recordMap.collectionsMap?.keys?.firstOrNull() ?: return null
val collectionViewId = page.recordMap.collectionViewsMap?.keys?.firstOrNull() ?: return null

val collectionResponse = queryCollection(collectionId, collectionViewId)

return collectionResponse.mapTable(json, sortColumns = sortColumns)
}

override suspend fun loadPage(pageId: String, limit: Int): NotionResponse =
LoadPageChunkRequest(_httpClient).execute(
LoadPageChunkRequestBody(pageId, limit, 0, false)
)

override suspend fun queryCollection(
collectionId: String,
collectionViewId: String,
limit: Int
): NotionResponse =
QueryNotionCollectionRequest(_httpClient).execute(
QueryCollectionRequestBody(
collectionId, collectionViewId, Loader(limit, false, "table")
)
)

override fun setHttpClient(newHttpClient: HttpClient) {
close()
_httpClient = newHttpClient.withToken()
}

override fun setToken(token: String) {
_token.set(token)
}

override fun close(): Unit =
_httpClient.close()

private fun HttpClient.withToken(): HttpClient =
config {
defaultRequest {
header(HttpHeaders.Cookie, "${Notion.TOKEN_COOKIE_KEY}=${_token.get()}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement

fun NotionCollection.title() = value.name?.trimNotionTextField()
public fun NotionCollection.title(): String? =
value.name?.trimNotionTextField()

fun NotionCollection.description() = value.description?.trimNotionTextField()
public fun NotionCollection.description(): String? =
value.description?.trimNotionTextField()

fun parseNotionColumn(json: Json, name: String, type: NotionColumnType, field: JsonArray?): NotionColumn {
if (field.isNullOrEmpty()) return NotionColumn.SingleValue(name, type, null)
val masterList = field.filterIsInstance<JsonArray>()
public fun JsonArray?.parseNotionColumn(json: Json, name: String, type: NotionColumnType): NotionColumn {
if (isNullOrEmpty()) return NotionColumn.SingleValue(name, type, null)
val masterList = filterIsInstance<JsonArray>()

return when (type) {
NotionColumnType.Title, NotionColumnType.Text, NotionColumnType.Number,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.petersamokhin.notionapi.mapper

import com.petersamokhin.notionapi.model.*
import com.petersamokhin.notionapi.model.NotionColumn
import com.petersamokhin.notionapi.model.NotionRow
import com.petersamokhin.notionapi.model.NotionTable
import com.petersamokhin.notionapi.model.response.NotionBlock
import com.petersamokhin.notionapi.model.response.NotionCollection
import com.petersamokhin.notionapi.model.response.NotionResponse
import kotlinx.serialization.json.*
import kotlinx.serialization.json.Json

fun NotionResponse.mapTable(json: Json, sortColumns: Boolean = false): NotionTable? {
public fun NotionResponse.mapTable(json: Json, sortColumns: Boolean = false): NotionTable? {
val collectionId = recordMap.collectionsMap?.keys?.firstOrNull() ?: return null
val collection = recordMap.collectionsMap[collectionId]

Expand All @@ -25,7 +27,7 @@ fun NotionResponse.mapTable(json: Json, sortColumns: Boolean = false): NotionTab
return blocks?.let { collection?.mapTable(json, it, sortMap) }
}

fun NotionCollection.mapTable(
public fun NotionCollection.mapTable(
json: Json,
blocks: List<NotionBlock>,
sortMap: Map<String, Int>? = null
Expand All @@ -47,7 +49,7 @@ fun NotionCollection.mapTable(
schemaItem.name.also { name ->
val field = props[innerRowKey]

rowItems[name] = parseNotionColumn(json, name, schemaItem.type, field)
rowItems[name] = field.parseNotionColumn(json, name, schemaItem.type)
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/com/petersamokhin/notionapi/model/NotionColumn.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.petersamokhin.notionapi.model

import com.petersamokhin.notionapi.model.response.NotionColumnType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
public sealed class NotionColumn {
public abstract val name: String
public abstract val type: NotionColumnType

@Serializable
@SerialName("single_value")
public data class SingleValue(
override val name: String,
override val type: NotionColumnType,
val value: NotionProperty?
) : NotionColumn()

@Serializable
@SerialName("multi_value")
public data class MultiValue(
override val name: String,
override val type: NotionColumnType,
val values: List<NotionProperty>
) : NotionColumn()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.petersamokhin.notionapi.model
import kotlinx.serialization.Serializable

@Serializable
data class NotionCredentials(
public data class NotionCredentials(
val email: String,
val password: String
)
)
Loading

0 comments on commit 614326e

Please sign in to comment.