├── settings.gradle ├── run └── config.yaml.sample ├── gradle.properties ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── kotlin │ │ └── com │ │ └── maaxgr │ │ └── todoistnotionsync │ │ ├── interfaces │ │ ├── todoistrepo │ │ │ ├── entities │ │ │ │ ├── sync │ │ │ │ │ ├── TempIdMapping.kt │ │ │ │ │ ├── TodoistSync.kt │ │ │ │ │ └── Item.kt │ │ │ │ ├── getacitivty │ │ │ │ │ ├── Activity.kt │ │ │ │ │ ├── ExtraData.kt │ │ │ │ │ └── Event.kt │ │ │ │ └── addtask │ │ │ │ │ └── AddTaskResponse.kt │ │ │ ├── TodoistRepo.kt │ │ │ └── TodoistRepoImpl.kt │ │ ├── notionrepo │ │ │ ├── databasequery │ │ │ │ ├── Text.kt │ │ │ │ ├── Parent.kt │ │ │ │ ├── Done.kt │ │ │ │ ├── Name.kt │ │ │ │ ├── Sync.kt │ │ │ │ ├── Properties.kt │ │ │ │ ├── DatabaseQuery.kt │ │ │ │ ├── Title.kt │ │ │ │ ├── Annotations.kt │ │ │ │ └── Result.kt │ │ │ ├── NotionRepo.kt │ │ │ └── NotionRepoImpl.kt │ │ ├── notionconfigdatabase │ │ │ ├── NotionConfigDatabase.kt │ │ │ └── NotionConfigDatabaseImpl.kt │ │ ├── sync │ │ │ └── integrator │ │ │ │ ├── Integrator.kt │ │ │ │ ├── CompletedTodoistItemIntegrator.kt │ │ │ │ ├── UncompletedTodoistItemIntegrator.kt │ │ │ │ ├── UpdatedTodoistItemIntegrator.kt │ │ │ │ ├── AddedTodoistItemIntegrator.kt │ │ │ │ └── helper │ │ │ │ └── TodoistToNotionUpdateHelper.kt │ │ ├── config │ │ │ ├── ConfigEntities.kt │ │ │ └── ConfigLoader.kt │ │ └── synctable │ │ │ ├── SyncTable.kt │ │ │ └── SyncTableImpl.kt │ │ ├── utils │ │ └── AppExtensions.kt │ │ ├── todoistmanager │ │ └── TodoistManager.kt │ │ ├── Main.kt │ │ └── SyncApplication.kt └── test │ └── kotlin │ ├── Tests.kt │ ├── TodoistTest.kt │ └── NotionClientTest.kt ├── README.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'TodoistNotionSync' 2 | 3 | -------------------------------------------------------------------------------- /run/config.yaml.sample: -------------------------------------------------------------------------------- 1 | notion: 2 | token: 3 | todoist: 4 | token: -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin_version=1.5.0 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | run/config.yaml 5 | src/test/resources/config.yaml -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaaxGr/NotionTodoistSync/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/sync/TempIdMapping.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync 2 | 3 | class TempIdMapping( 4 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Text.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Text( 4 | val content: String, 5 | val link: Any 6 | ) 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Parent.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Parent( 4 | val database_id: String, 5 | val type: String 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/getacitivty/Activity.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty 2 | 3 | data class Activity( 4 | val count: Int, 5 | val events: List 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Done.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Done( 4 | val checkbox: Boolean, 5 | val id: String, 6 | val type: String 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Name.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Name( 4 | val id: String, 5 | val title: List, 6 | val type: String 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Sync.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Sync( 4 | val checkbox: Boolean, 5 | val id: String, 6 | val type: String 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/getacitivty/ExtraData.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty 2 | 3 | data class ExtraData( 4 | val client: String, 5 | val content: String 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Properties.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Properties( 4 | val Done: Done, 5 | val Name: Name, 6 | val Sync: Sync 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionconfigdatabase/NotionConfigDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionconfigdatabase 2 | 3 | interface NotionConfigDatabase { 4 | suspend fun getValue(key: String): String? 5 | suspend fun setValue(key: String, value: String) 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/DatabaseQuery.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class DatabaseQuery( 4 | val has_more: Boolean, 5 | val next_cursor: Any, 6 | val `object`: String, 7 | val results: List<Result> 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/sync/TodoistSync.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync 2 | 3 | data class TodoistSync( 4 | val full_sync: Boolean, 5 | val items: List<Item>, 6 | val sync_token: String, 7 | val temp_id_mapping: TempIdMapping 8 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Title.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Title( 4 | val annotations: Annotations, 5 | val href: Any, 6 | val plain_text: String, 7 | val text: Text, 8 | val type: String 9 | ) 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/sync/integrator/Integrator.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.sync.integrator 2 | 3 | interface Integrator { 4 | 5 | val integratorName: String 6 | 7 | suspend fun integrate() 8 | 9 | fun log(message: String) { 10 | println("[$integratorName] $message") 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Annotations.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Annotations( 4 | val bold: Boolean, 5 | val code: Boolean, 6 | val color: String, 7 | val italic: Boolean, 8 | val strikethrough: Boolean, 9 | val underline: Boolean 10 | ) 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/databasequery/Result.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery 2 | 3 | data class Result( 4 | val archived: Boolean, 5 | val created_time: String, 6 | val id: String, 7 | val last_edited_time: String, 8 | val `object`: String, 9 | val parent: Parent, 10 | val properties: Properties 11 | ) 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/getacitivty/Event.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty 2 | 3 | data class Event( 4 | val event_date: String, 5 | val event_type: String, 6 | val extra_data: ExtraData, 7 | val id: Long, 8 | val initiator_id: Any, 9 | val object_id: Long, 10 | val object_type: String, 11 | val parent_item_id: Any, 12 | val parent_project_id: Long 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/config/ConfigEntities.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.config 2 | 3 | import kotlinx.serialization.Serializable as Serializable 4 | 5 | @Serializable 6 | data class ConfigYaml( 7 | val notion: ConfigNotion, 8 | val todoist: ConfigTodoist 9 | ) 10 | 11 | @Serializable 12 | data class ConfigNotion( 13 | val token: String 14 | ) 15 | 16 | @Serializable 17 | data class ConfigTodoist( 18 | val token: String 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/addtask/AddTaskResponse.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.addtask 2 | 3 | data class AddTaskResponse( 4 | val assigner: Int, 5 | val comment_count: Int, 6 | val completed: Boolean, 7 | val content: String, 8 | val created: String, 9 | val creator: Int, 10 | val description: String, 11 | val id: Long, 12 | val label_ids: List<Any>, 13 | val order: Int, 14 | val priority: Int, 15 | val project_id: Long, 16 | val section_id: Int, 17 | val url: String 18 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/utils/AppExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.utils 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | val DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS = "yyyy-MM-dd'T'HH:mm:ssX" 7 | val DATE_FORMAT_ISO8601_WITHMS_AND_WITHTS = "yyyy-MM-dd'T'HH:mm:ss.SSSX" 8 | 9 | fun String.asDate(format: String, forceGMT: Boolean = false): Date { 10 | return SimpleDateFormat(format) 11 | .apply { 12 | if (forceGMT) { 13 | timeZone = TimeZone.getTimeZone("GMT") 14 | } 15 | } 16 | .parse(this) 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/NotionRepo.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo 2 | 3 | import org.jraf.klibnotion.model.base.UuidString 4 | import org.jraf.klibnotion.model.date.Date 5 | import org.jraf.klibnotion.model.date.Timestamp 6 | import org.jraf.klibnotion.model.page.Page 7 | 8 | interface NotionRepo { 9 | data class Entry( 10 | val pageId: UuidString, 11 | val lastEditedTime: Timestamp 12 | ) 13 | 14 | suspend fun getDatabaseEntries(queryBody: String): List<Entry> 15 | 16 | suspend fun update(pageId: UuidString, title: String): Timestamp 17 | 18 | suspend fun check(pageId: UuidString): Timestamp 19 | 20 | suspend fun uncheck(pageId: UuidString): Timestamp 21 | suspend fun add(name: String): Page 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/entities/sync/Item.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync 2 | 3 | data class Item( 4 | val added_by_uid: Int, 5 | val assigned_by_uid: Any, 6 | val checked: Int, 7 | val child_order: Int, 8 | val collapsed: Int, 9 | val content: String, 10 | val date_added: String, 11 | val date_completed: Any, 12 | val day_order: Int, 13 | val description: String, 14 | val due: Any, 15 | val id: Long, 16 | val in_history: Int, 17 | val is_deleted: Int, 18 | val labels: List<Any>, 19 | val parent_id: Any, 20 | val priority: Int, 21 | val project_id: Long, 22 | val responsible_uid: Any, 23 | val section_id: Any, 24 | val sync_id: Any, 25 | val user_id: Int 26 | ) -------------------------------------------------------------------------------- /src/test/kotlin/Tests.kt: -------------------------------------------------------------------------------- 1 | import org.junit.Test 2 | import java.text.SimpleDateFormat 3 | import java.util.* 4 | 5 | class Tests { 6 | 7 | @Test 8 | fun test() { 9 | 10 | val notionLastUpdate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX").parse("2021-07-18T16:19:44Z") 11 | val notionLastUpdate2 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse("2021-07-18T16:19:44Z") 12 | val notionLastUpdate3 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").apply { timeZone = TimeZone.getTimeZone("GMT") }.parse("2021-07-18T16:19:44Z") 13 | 14 | println("A: ${SimpleDateFormat("HH:mm:ss").format(notionLastUpdate)}") 15 | println("B: ${SimpleDateFormat("HH:mm:ss").format(notionLastUpdate2)}") 16 | println("B: ${SimpleDateFormat("HH:mm:ss").format(notionLastUpdate3)}") 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/TodoistRepo.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.addtask.AddTaskResponse 4 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 5 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync.Item 6 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync.TodoistSync 7 | 8 | interface TodoistRepo { 9 | fun getTodoistEntries(): List<Item> 10 | fun setNotionEntryIdInTodoistEntry(todoistId: Long, notionId: String): Boolean 11 | fun createEntry(content: String): AddTaskResponse 12 | fun updateContent(todoistId: Long, content: String): Item 13 | 14 | fun readActivity(limit: Int, offset: Int): List<Event> 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/synctable/SyncTable.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.synctable 2 | 3 | import java.util.* 4 | 5 | 6 | interface SyncTable { 7 | 8 | 9 | data class SyncTableEntry( 10 | val pageId: String, 11 | var notionId: String, 12 | var todoistId: Long, 13 | var todoistLastUpdate: Date?, 14 | var notionLastUpdate: Date? 15 | ) 16 | 17 | data class AddSyncTableEntry( 18 | val notionId: String, 19 | val todoistId: Long, 20 | var todoistLastUpdate: Date, 21 | var notionLastUpdate: Date 22 | ) 23 | 24 | suspend fun getSyncTable(): List<SyncTableEntry> 25 | suspend fun reloadSyncTable() 26 | suspend fun addSyncEntry(values: AddSyncTableEntry) 27 | suspend fun deleteSyncEntry(pageId: String) 28 | suspend fun updateSyncEntry(values: SyncTableEntry) 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sync Todoist with Notion Database 2 | 3 | Sync a notion database with todoist. 4 | This project is work in progress. 5 | 6 | ## Important Update 7 | 8 | This project is stale and i have no time to continue this at the moment! 9 | I am sorry. 10 | 11 | 12 | ## ToDo 13 | 14 | - [x] Fetch Database Items from Notion 15 | - [x] Fetch Database Items + Activity from Todoist 16 | - [ ] Synchronize Changes (Title + Checkbox) 17 | - [x] Sync Edits in Title from Todoist to Notion 18 | - [ ] Sync Checked State from Todoist to Notion 19 | - [ ] Sync Edits in Title from Notion to Todoist 20 | - [ ] Sync Checked State from Notion to Todoist 21 | - [ ] Sync Add-Task-Operation in Notion to Todoist 22 | - [ ] Sync Add-Task-Operation in Todoist to Notion 23 | - [ ] Sync Delete-Task-Operation in Notion to Todoist 24 | - [ ] Sync Delete-Task-Operation in Todoist to Notion 25 | - [ ] Make Database configurable 26 | 27 | ## Other Notion Integrations 28 | 29 | Check out this repository to see other open source integrations: 30 | https://github.com/MaaxGr/open-source-notionapi-apps 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/todoistmanager/TodoistManager.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.todoistmanager 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.TodoistRepo 4 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.inject 7 | 8 | class TodoistManager: KoinComponent { 9 | 10 | private val todoistRepo: TodoistRepo by inject() 11 | 12 | fun getUpdatesToProcess(lastId: Long): List<Event> { 13 | val updates: MutableList<Event> = mutableListOf() 14 | var pageId = 1 15 | while (true) { 16 | val moreUpdates = todoistRepo.readActivity(10, (pageId - 1) * 10) 17 | 18 | if (moreUpdates.isEmpty()) { 19 | return updates 20 | } 21 | 22 | val moreMinID = moreUpdates.minOf { it.id } 23 | 24 | updates.addAll(moreUpdates) 25 | 26 | if (moreMinID <= lastId) { 27 | return updates.filter { it.id > lastId }.sortedBy { it.id } 28 | } 29 | 30 | pageId++ 31 | } 32 | } 33 | 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/sync/integrator/CompletedTodoistItemIntegrator.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.sync.integrator 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.Result 5 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.helper.TodoistToNotionUpdateHelper 6 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 7 | import org.koin.core.component.KoinComponent 8 | import org.koin.core.component.inject 9 | 10 | class CompletedTodoistItemIntegrator( 11 | private val update: Event, 12 | private val notionEntries: List<NotionRepo.Entry> 13 | ) : Integrator, KoinComponent { 14 | 15 | private val notionRepo: NotionRepo by inject() 16 | 17 | override val integratorName: String 18 | get() = "COMPLETED_TODOIST_INTEGRATOR" 19 | 20 | private val helper = TodoistToNotionUpdateHelper( 21 | notionEntries = notionEntries, 22 | todoistEvent = update, 23 | log = ::log, 24 | notionUpdate = { notionRepo.check(it.pageId) } 25 | ) 26 | 27 | override suspend fun integrate() { 28 | helper.check() 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/config/ConfigLoader.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.config 2 | 3 | import com.charleskorn.kaml.PolymorphismStyle 4 | import com.charleskorn.kaml.Yaml 5 | import kotlinx.serialization.decodeFromString 6 | import java.io.File 7 | 8 | class ConfigLoader() { 9 | 10 | var customDir = "" 11 | 12 | companion object { 13 | const val CONFIG_FILE_NAME = "config.yaml" 14 | } 15 | 16 | fun loadConfig(): ConfigYaml { 17 | val configFileContent = loadConfigContent() 18 | val yamlParser = createYamlParser() 19 | 20 | return yamlParser.decodeFromString(configFileContent) 21 | } 22 | 23 | private fun loadConfigContent(): String { 24 | val file = if (customDir.isNotBlank()) { 25 | File(customDir, CONFIG_FILE_NAME) 26 | } else { 27 | File(CONFIG_FILE_NAME) 28 | } 29 | return file.readText() 30 | } 31 | 32 | private fun createYamlParser(): Yaml { 33 | val yamlConfig = Yaml.default.configuration.copy( 34 | polymorphismStyle = PolymorphismStyle.Property, 35 | polymorphismPropertyName = "type", 36 | ) 37 | return Yaml(Yaml.default.serializersModule, yamlConfig) 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/sync/integrator/UncompletedTodoistItemIntegrator.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.sync.integrator 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.Result 5 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.helper.TodoistToNotionUpdateHelper 6 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 7 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 8 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHMS_AND_WITHTS 9 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS 10 | import com.maaxgr.todoistnotionsync.utils.asDate 11 | import org.koin.core.component.KoinComponent 12 | import org.koin.core.component.inject 13 | import java.util.* 14 | 15 | class UncompletedTodoistItemIntegrator( 16 | private val update: Event, 17 | private val notionEntries: List<NotionRepo.Entry> 18 | ) : Integrator, KoinComponent { 19 | 20 | private val notionRepo: NotionRepo by inject() 21 | private val syncTableService: SyncTable by inject() 22 | 23 | private val helper = TodoistToNotionUpdateHelper( 24 | notionEntries = notionEntries, 25 | todoistEvent = update, 26 | log = ::log, 27 | notionUpdate = { notionRepo.uncheck(it.pageId) } 28 | ) 29 | 30 | override val integratorName: String 31 | get() = "UNCOMPLETED_TODOIST_INTEGRATOR" 32 | 33 | 34 | override suspend fun integrate() { 35 | helper.check() 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/TodoistTest.kt: -------------------------------------------------------------------------------- 1 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigLoader 2 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigNotion 3 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigYaml 4 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.TodoistRepo 5 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.TodoistRepoImpl 6 | import com.maaxgr.todoistnotionsync.todoistmanager.TodoistManager 7 | import org.jraf.klibnotion.client.Authentication 8 | import org.jraf.klibnotion.client.ClientConfiguration 9 | import org.jraf.klibnotion.client.NotionClient 10 | import org.junit.After 11 | import org.junit.Before 12 | import org.junit.Test 13 | import org.koin.core.context.startKoin 14 | import org.koin.core.context.stopKoin 15 | import org.koin.dsl.module 16 | 17 | class TodoistTest { 18 | 19 | @Before 20 | fun before() { 21 | startKoin { 22 | val mainModule = module { 23 | single { 24 | ConfigLoader().run { 25 | customDir = "src/test/resources" 26 | loadConfig() 27 | } 28 | } 29 | single { get<ConfigYaml>().todoist } 30 | single<TodoistRepo> { TodoistRepoImpl() } 31 | } 32 | modules(mainModule) 33 | } 34 | } 35 | 36 | @After 37 | fun after() { 38 | stopKoin() 39 | } 40 | 41 | @Test 42 | fun test() { 43 | val todoistManager = TodoistManager() 44 | 45 | val updates = todoistManager.getUpdatesToProcess(12568587997) 46 | println("Size: ${updates.size}") 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/sync/integrator/UpdatedTodoistItemIntegrator.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.sync.integrator 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.Result 5 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.helper.TodoistToNotionUpdateHelper 6 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 7 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 8 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHMS_AND_WITHTS 9 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS 10 | import com.maaxgr.todoistnotionsync.utils.asDate 11 | import org.jraf.klibnotion.model.page.Page 12 | import org.koin.core.component.KoinComponent 13 | import org.koin.core.component.inject 14 | import java.util.* 15 | 16 | class UpdatedTodoistItemIntegrator( 17 | private val update: Event, 18 | private val notionEntries: List<NotionRepo.Entry> 19 | ) : Integrator, KoinComponent { 20 | 21 | private val notionRepo: NotionRepo by inject() 22 | private val syncTableService: SyncTable by inject() 23 | 24 | private val helper = TodoistToNotionUpdateHelper( 25 | notionEntries = notionEntries, 26 | todoistEvent = update, 27 | log = ::log, 28 | notionUpdate = { notionRepo.update(it.pageId, it.name) } 29 | ) 30 | 31 | override val integratorName: String 32 | get() = "UPDATE_TODOIST_INTEGRATOR" 33 | 34 | override suspend fun integrate() { 35 | helper.check() 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/sync/integrator/AddedTodoistItemIntegrator.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.sync.integrator 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.Result 5 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.helper.TodoistToNotionUpdateHelper 6 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 7 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 8 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHMS_AND_WITHTS 9 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS 10 | import com.maaxgr.todoistnotionsync.utils.asDate 11 | import org.koin.core.component.KoinComponent 12 | import org.koin.core.component.inject 13 | import java.util.* 14 | 15 | class AddedTodoistItemIntegrator( 16 | private val event: Event, 17 | private val notionEntries: List<NotionRepo.Entry> 18 | ) : Integrator, KoinComponent { 19 | 20 | private val notionRepo: NotionRepo by inject() 21 | private val syncTableService: SyncTable by inject() 22 | 23 | override val integratorName: String 24 | get() = "ADD_TODOIST_INTEGRATOR" 25 | 26 | override suspend fun integrate() { 27 | log("Processing add event ${event.id}") 28 | 29 | val eventDate = event.event_date.asDate(DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS) 30 | 31 | val newPage = notionRepo.add(event.extra_data.content) 32 | 33 | syncTableService.addSyncEntry( 34 | SyncTable.AddSyncTableEntry( 35 | notionId = newPage.id, 36 | todoistId = event.object_id, 37 | todoistLastUpdate = eventDate, 38 | notionLastUpdate = eventDate, 39 | )) 40 | 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/Main.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigLoader 4 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigNotion 5 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigYaml 6 | import com.maaxgr.todoistnotionsync.interfaces.notionconfigdatabase.NotionConfigDatabase 7 | import com.maaxgr.todoistnotionsync.interfaces.notionconfigdatabase.NotionConfigDatabaseImpl 8 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 9 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepoImpl 10 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 11 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTableImpl 12 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.TodoistRepo 13 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.TodoistRepoImpl 14 | import com.maaxgr.todoistnotionsync.todoistmanager.TodoistManager 15 | import kotlinx.coroutines.runBlocking 16 | import org.jraf.klibnotion.client.Authentication 17 | import org.jraf.klibnotion.client.ClientConfiguration 18 | import org.jraf.klibnotion.client.NotionClient 19 | import org.koin.core.context.startKoin 20 | import org.koin.dsl.module 21 | import kotlin.system.exitProcess 22 | 23 | fun main() { 24 | 25 | startKoin { 26 | val module = module { 27 | single { ConfigLoader().loadConfig() } 28 | single { get<ConfigYaml>().notion } 29 | single { get<ConfigYaml>().todoist } 30 | single<NotionRepo> { NotionRepoImpl() } 31 | single<TodoistRepo> { TodoistRepoImpl() } 32 | single { NotionClient.newInstance(ClientConfiguration(Authentication(get<ConfigNotion>().token))) } 33 | single<SyncTable> { SyncTableImpl() } 34 | single<NotionConfigDatabase> { NotionConfigDatabaseImpl() } 35 | single { TodoistManager() } 36 | } 37 | modules(module) 38 | } 39 | 40 | runBlocking { 41 | SyncApplication().init() 42 | exitProcess(0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/kotlin/NotionClientTest.kt: -------------------------------------------------------------------------------- 1 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigLoader 2 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigNotion 3 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigYaml 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepoImpl 5 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 6 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTableImpl 7 | import kotlinx.coroutines.runBlocking 8 | import org.jraf.klibnotion.client.Authentication 9 | import org.jraf.klibnotion.client.ClientConfiguration 10 | import org.jraf.klibnotion.client.NotionClient 11 | import org.junit.After 12 | import org.junit.Before 13 | import org.junit.Test 14 | import org.koin.core.context.startKoin 15 | import org.koin.core.context.stopKoin 16 | import org.koin.dsl.module 17 | import java.util.* 18 | 19 | class NotionClientTest { 20 | 21 | @Before 22 | fun before() { 23 | startKoin { 24 | val mainModule = module { 25 | single { 26 | ConfigLoader().run { 27 | customDir = "src/test/resources" 28 | loadConfig() 29 | } 30 | } 31 | single { get<ConfigYaml>().notion } 32 | single { NotionClient.newInstance(ClientConfiguration(Authentication(get<ConfigNotion>().token))) } 33 | } 34 | modules(mainModule) 35 | } 36 | } 37 | 38 | @After 39 | fun after() { 40 | stopKoin() 41 | } 42 | 43 | @Test 44 | fun test(): Unit = runBlocking { 45 | val syncTableImpl = SyncTableImpl() 46 | 47 | syncTableImpl.reloadSyncTable() 48 | 49 | syncTableImpl.getSyncTable().forEach { println(it) } 50 | 51 | //repo.addToSyncTable(NotionRepo.AddSyncTableValues("COOL", "GEHT")) 52 | 53 | // repo.updateTodoistId(NotionRepo.SyncTableValues( 54 | // pageId = "9d2b14c2-4cc7-45aa-9ddb-a7ab88a2d823", 55 | // notionId = "abc update", 56 | // todoistId = "def update" 57 | // )) 58 | 59 | 60 | // syncTableImpl.updateSyncEntry( 61 | // SyncTable.SyncTableEntry( 62 | // pageId = "9d2b14c2-4cc7-45aa-9ddb-a7ab88a2d823", 63 | // notionId = "abc update", 64 | // todoistId = "def update", 65 | // todoistLastUpdate = Date(System.currentTimeMillis()) 66 | // ) 67 | // ) 68 | 69 | //syncTableImpl.deleteSyncEntry("85b03c51-cf16-4074-aabd-8dca29906dcc") 70 | //syncTableImpl.deleteSyncEntry("57f80fbd-8715-420f-b5ef-08a0d6410557") 71 | 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionrepo/NotionRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionrepo 2 | 3 | import com.github.kittinunf.fuel.Fuel 4 | import com.github.kittinunf.fuel.core.extensions.jsonBody 5 | import com.github.kittinunf.fuel.gson.responseObject 6 | import com.github.kittinunf.result.Result 7 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigNotion 8 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.DatabaseQuery 9 | import org.jraf.klibnotion.client.NotionClient 10 | import org.jraf.klibnotion.model.base.UuidString 11 | import org.jraf.klibnotion.model.base.reference.DatabaseReference 12 | import org.jraf.klibnotion.model.date.Date 13 | import org.jraf.klibnotion.model.date.Timestamp 14 | import org.jraf.klibnotion.model.page.Page 15 | import org.jraf.klibnotion.model.pagination.ResultPage 16 | import org.jraf.klibnotion.model.property.value.PropertyValueList 17 | import org.koin.core.component.KoinComponent 18 | import org.koin.core.component.inject 19 | 20 | class NotionRepoImpl : NotionRepo, KoinComponent { 21 | 22 | private val notionConfig: ConfigNotion by inject() 23 | private val notionClient: NotionClient by inject() 24 | 25 | private val tableToSync: UuidString = "9123f835-37e4-4c5e-afeb-3c8fb3655f44" 26 | 27 | 28 | fun load() { 29 | 30 | } 31 | 32 | 33 | override suspend fun getDatabaseEntries(queryBody: String): List<NotionRepo.Entry> { 34 | return notionClient.databases.queryDatabase(tableToSync).results.map { 35 | NotionRepo.Entry( 36 | pageId = it.id, 37 | lastEditedTime = it.lastEdited 38 | ) 39 | } 40 | } 41 | 42 | override suspend fun update(pageId: UuidString, title: String): Timestamp { 43 | val response = notionClient.pages.updatePage(pageId, PropertyValueList() 44 | .title("Name", title)) 45 | 46 | return response.lastEdited 47 | } 48 | 49 | override suspend fun check(pageId: UuidString): Timestamp { 50 | val response = notionClient.pages.updatePage(pageId, PropertyValueList() 51 | .checkbox("Done", true)) 52 | 53 | return response.lastEdited 54 | } 55 | 56 | override suspend fun uncheck(pageId: UuidString): Timestamp { 57 | val response = notionClient.pages.updatePage(pageId, PropertyValueList() 58 | .checkbox("Done", false)) 59 | 60 | return response.lastEdited 61 | } 62 | 63 | override suspend fun add(name: String): Page { 64 | return notionClient.pages.createPage( 65 | DatabaseReference(tableToSync), PropertyValueList() 66 | .title("Name", name)) 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/sync/integrator/helper/TodoistToNotionUpdateHelper.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.sync.integrator.helper 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.Result 5 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 6 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 7 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHMS_AND_WITHTS 8 | import com.maaxgr.todoistnotionsync.utils.DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS 9 | import com.maaxgr.todoistnotionsync.utils.asDate 10 | import org.jraf.klibnotion.model.base.UuidString 11 | import org.jraf.klibnotion.model.page.Page 12 | import org.koin.core.component.KoinComponent 13 | import org.koin.core.component.inject 14 | import java.sql.Timestamp 15 | import java.util.* 16 | 17 | class TodoistToNotionUpdateHelper( 18 | private val notionEntries: List<NotionRepo.Entry>, 19 | private val todoistEvent: Event, 20 | private val log: (String) -> Unit, 21 | private val notionUpdate: suspend (payload: UpdatePayload) -> Date 22 | ): KoinComponent { 23 | 24 | private val syncTableService: SyncTable by inject() 25 | private val notionRepo: NotionRepo by inject() 26 | 27 | suspend fun check() { 28 | log("Processing update event ${todoistEvent.id}") 29 | 30 | val todoistLastUpdate = todoistEvent.event_date.asDate(DATE_FORMAT_ISO8601_WITHOUTMS_AND_WITHTS) 31 | val notionSyncEntry = syncTableService.getSyncTable().firstOrNull { it.todoistId == todoistEvent.object_id } 32 | if (notionSyncEntry == null) { 33 | log("Can't update notion for ${todoistEvent.id}. No id in sync table") 34 | return 35 | } 36 | 37 | val entry = notionEntries.firstOrNull { it.pageId == notionSyncEntry.notionId } 38 | if (entry == null) { 39 | log("Can't update notion for ${todoistEvent.id}. No entry in data table") 40 | return 41 | } 42 | 43 | val notionLastUpdate = Date(entry.lastEditedTime.time) 44 | 45 | if (todoistLastUpdate.time > notionLastUpdate.time) { 46 | val updatedTimestamp = notionUpdate(UpdatePayload(entry.pageId, todoistEvent.extra_data.content)) 47 | log("Applied update ${todoistEvent.id}") 48 | 49 | syncTableService.updateSyncEntry(notionSyncEntry.copy( 50 | todoistLastUpdate = todoistLastUpdate, 51 | notionLastUpdate = Date(updatedTimestamp.time) 52 | )) 53 | log("Updated sync table for update ${todoistEvent.id}") 54 | 55 | } else { 56 | log("Don't update notion for ${todoistEvent.id}. Notion update date is newer") 57 | } 58 | } 59 | 60 | data class UpdatePayload( 61 | val pageId: UuidString, 62 | val name: String 63 | ) 64 | 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/notionconfigdatabase/NotionConfigDatabaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.notionconfigdatabase 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigNotion 4 | import org.jraf.klibnotion.client.NotionClient 5 | import org.jraf.klibnotion.model.base.UuidString 6 | import org.jraf.klibnotion.model.base.reference.DatabaseReference 7 | import org.jraf.klibnotion.model.database.query.DatabaseQuery 8 | import org.jraf.klibnotion.model.database.query.filter.DatabaseQueryPredicate 9 | import org.jraf.klibnotion.model.database.query.filter.DatabaseQueryPropertyFilter 10 | import org.jraf.klibnotion.model.property.value.PropertyValueList 11 | import org.jraf.klibnotion.model.property.value.RichTextPropertyValue 12 | import org.koin.core.component.KoinComponent 13 | import org.koin.core.component.inject 14 | import org.koin.java.KoinJavaComponent.inject 15 | import javax.xml.crypto.Data 16 | 17 | class NotionConfigDatabaseImpl : NotionConfigDatabase, KoinComponent { 18 | 19 | private val notionConfig: ConfigNotion by inject() 20 | private val databaseId: UuidString = "046d1c48-0347-41b8-a503-ad2d2fb4c976" 21 | private val notionClient: NotionClient by inject() 22 | 23 | override suspend fun getValue(key: String): String? { 24 | 25 | val filter = DatabaseQueryPropertyFilter.Title( 26 | propertyIdOrName = "config_key", 27 | predicate = DatabaseQueryPredicate.Text.Equals(key) 28 | ) 29 | 30 | val results = notionClient.databases.queryDatabase(databaseId, DatabaseQuery().any(filter)).results 31 | if (results.isEmpty()) { 32 | return null 33 | } 34 | 35 | val valueColumn = results.first().propertyValues.first { it.name == "config_value" } as RichTextPropertyValue 36 | return valueColumn.value.plainText 37 | } 38 | 39 | private suspend fun getPageId(key: String): UuidString? { 40 | val filter = DatabaseQueryPropertyFilter.Title( 41 | propertyIdOrName = "config_key", 42 | predicate = DatabaseQueryPredicate.Text.Equals(key) 43 | ) 44 | 45 | val results = notionClient.databases.queryDatabase(databaseId, DatabaseQuery().any(filter)).results 46 | if (results.isEmpty()) { 47 | return null 48 | } 49 | 50 | return results.first().id 51 | } 52 | 53 | override suspend fun setValue(key: String, value: String) { 54 | val pageId = getPageId(key) 55 | 56 | if (pageId == null) { 57 | notionClient.pages.createPage( 58 | parentDatabase = DatabaseReference(databaseId), 59 | properties = PropertyValueList() 60 | .title("config_key", key) 61 | .text("config_value", value) 62 | ) 63 | } else { 64 | notionClient.pages.updatePage(pageId, PropertyValueList().text("config_value", value)) 65 | } 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/todoistrepo/TodoistRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.todoistrepo 2 | 3 | import com.github.kittinunf.fuel.Fuel 4 | import com.github.kittinunf.fuel.gson.jsonBody 5 | import com.github.kittinunf.fuel.gson.responseObject 6 | import com.github.kittinunf.result.Result 7 | import com.maaxgr.todoistnotionsync.interfaces.config.ConfigTodoist 8 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.addtask.AddTaskResponse 9 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Activity 10 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 11 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync.Item 12 | import org.koin.core.component.KoinComponent 13 | import org.koin.core.component.inject 14 | import java.lang.Exception 15 | 16 | class TodoistRepoImpl : TodoistRepo, KoinComponent { 17 | 18 | private val todoistConfig: ConfigTodoist by inject() 19 | 20 | override fun getTodoistEntries(): List<Item> { 21 | val (request, response, result) = Fuel.post("https://api.todoist.com/rest/v1/tasks") 22 | .header("Authorization", "Bearer ${todoistConfig.token}") 23 | .responseObject<List<Item>>() 24 | 25 | when(result) { 26 | is Result.Success -> { 27 | return result.value 28 | } 29 | is Result.Failure -> { 30 | throw result.error 31 | } 32 | } 33 | } 34 | 35 | fun getTodoistEntry(id: Long): Item { 36 | val (request, response, result) = Fuel.post("https://api.todoist.com/rest/v1/tasks/$id") 37 | .header("Authorization", "Bearer ${todoistConfig.token}") 38 | .responseObject<Item>() 39 | 40 | when(result) { 41 | is Result.Success -> { 42 | return result.value 43 | } 44 | is Result.Failure -> { 45 | throw result.error 46 | } 47 | } 48 | } 49 | 50 | 51 | override fun updateContent(todoistId: Long, content: String): Item { 52 | val body = mapOf("content" to content) 53 | 54 | val (request, response, result) = Fuel.post("https://api.todoist.com/rest/v1/tasks/$todoistId") 55 | .header("Authorization", "Bearer ${todoistConfig.token}") 56 | .jsonBody(body) 57 | .response() 58 | 59 | if (response.statusCode != 204) { 60 | throw Exception("Invalid Status Code ${response.statusCode}") 61 | } 62 | 63 | return getTodoistEntry(todoistId) 64 | } 65 | 66 | override fun setNotionEntryIdInTodoistEntry(todoistId: Long, notionId: String): Boolean { 67 | //TODO: Don't override complete description! 68 | val body = mapOf("description" to notionId) 69 | 70 | val (request, response, result) = Fuel.post("https://api.todoist.com/rest/v1/tasks/$todoistId") 71 | .header("Authorization", "Bearer ${todoistConfig.token}") 72 | .jsonBody(body) 73 | .response() 74 | 75 | return response.statusCode == 204 76 | } 77 | 78 | override fun createEntry(content: String): AddTaskResponse { 79 | //TODO: Prefix NotionId 80 | val body = mapOf( 81 | "content" to content, 82 | "description" to "", 83 | "project_id" to 2265619327 84 | ) 85 | 86 | val (request, response, result) = Fuel.post("https://api.todoist.com/rest/v1/tasks") 87 | .header("Authorization", "Bearer ${todoistConfig.token}") 88 | .jsonBody(body) 89 | .responseObject<AddTaskResponse>() 90 | 91 | when(result) { 92 | is Result.Success -> { 93 | return result.value 94 | } 95 | is Result.Failure -> { 96 | throw result.error 97 | } 98 | } 99 | } 100 | 101 | override fun readActivity(limit: Int, offset: Int): List<Event> { 102 | val (request, response, result) = Fuel.get("https://api.todoist.com/sync/v8/activity/get?limit=$limit&offset=$offset") 103 | .header("Authorization", "Bearer ${todoistConfig.token}") 104 | .responseObject<Activity>() 105 | 106 | when(result) { 107 | is Result.Success -> { 108 | return result.value.events 109 | } 110 | is Result.Failure -> { 111 | throw result.error 112 | } 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/interfaces/synctable/SyncTableImpl.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync.interfaces.synctable 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 4 | import org.jraf.klibnotion.client.NotionClient 5 | import org.jraf.klibnotion.model.base.UuidString 6 | import org.jraf.klibnotion.model.base.reference.DatabaseReference 7 | import org.jraf.klibnotion.model.date.DateOrDateRange 8 | import org.jraf.klibnotion.model.date.DateTime 9 | import org.jraf.klibnotion.model.date.Timestamp 10 | import org.jraf.klibnotion.model.property.value.* 11 | import org.koin.core.component.KoinComponent 12 | import org.koin.core.component.inject 13 | import java.text.SimpleDateFormat 14 | import java.util.* 15 | 16 | class SyncTableImpl : SyncTable, KoinComponent { 17 | 18 | private val notionRepo: NotionRepo by inject() 19 | private val syncDatabaseId: UuidString = "2c3f4342-89be-4e88-a6d0-a76efe31782a" 20 | private val notionClient: NotionClient by inject() 21 | 22 | private val syncTable: MutableList<SyncTable.SyncTableEntry> = mutableListOf() 23 | 24 | override suspend fun reloadSyncTable() { 25 | syncTable.clear() 26 | 27 | val result = notionClient.databases.queryDatabase(syncDatabaseId) 28 | 29 | val list = result.results.map { page -> 30 | val props = page.propertyValues 31 | 32 | val notionIdProperty = props.first { it.name == "notion_id" } as TitlePropertyValue 33 | val todoistProperty = props.first { it.name == "todoist_id" } as NumberPropertyValue 34 | val todoistUpdateTimeProperty = props.firstOrNull { it.name == "todoist_update_time" } as DatePropertyValue? 35 | val todoistlastUpdateDate = todoistUpdateTimeProperty?.value?.start?.timestamp 36 | 37 | val notionUpdateTimeProperty = props.firstOrNull { it.name == "notion_update_time" } as DatePropertyValue? 38 | val notionLastUpdateDate = notionUpdateTimeProperty?.value?.start?.timestamp 39 | 40 | SyncTable.SyncTableEntry( 41 | pageId = page.id, 42 | notionId = notionIdProperty.value.plainText ?: "", 43 | todoistId = todoistProperty.value.toLong(), 44 | todoistLastUpdate = todoistlastUpdateDate, 45 | notionLastUpdate = notionLastUpdateDate 46 | ) 47 | } 48 | 49 | syncTable.addAll(list) 50 | } 51 | 52 | override suspend fun getSyncTable(): List<SyncTable.SyncTableEntry> { 53 | return syncTable 54 | } 55 | 56 | override suspend fun addSyncEntry(values: SyncTable.AddSyncTableEntry) { 57 | val createdPage = notionClient.pages.createPage( 58 | parentDatabase = DatabaseReference(syncDatabaseId), 59 | properties = PropertyValueList() 60 | .title("notion_id", values.notionId) 61 | .number("todoist_id", values.todoistId) 62 | .date("todoist_update_time", DateOrDateRange(DateTime(Timestamp(values.todoistLastUpdate.time)), null)) 63 | .date("notion_update_time", DateOrDateRange(DateTime(Timestamp(values.notionLastUpdate.time)), null)) 64 | ) 65 | 66 | syncTable.add( 67 | SyncTable.SyncTableEntry( 68 | pageId = createdPage.id, 69 | notionId = values.notionId, 70 | todoistId = values.todoistId, 71 | todoistLastUpdate = values.todoistLastUpdate, 72 | notionLastUpdate = values.notionLastUpdate 73 | ) 74 | ) 75 | } 76 | 77 | override suspend fun deleteSyncEntry(pageId: String) { 78 | notionClient.pages.archivePage(pageId) 79 | syncTable.removeIf { it.pageId == pageId } 80 | } 81 | 82 | override suspend fun updateSyncEntry(values: SyncTable.SyncTableEntry) { 83 | val propertyList = PropertyValueList() 84 | .title("notion_id", values.notionId) 85 | .number("todoist_id", values.todoistId) 86 | 87 | println(TimeZone.getDefault()) 88 | 89 | println(SimpleDateFormat().format(Date(System.currentTimeMillis()))) 90 | 91 | 92 | values.todoistLastUpdate?.let { 93 | propertyList.date("todoist_update_time", DateOrDateRange(DateTime(Timestamp(it.time)), null)) 94 | } 95 | 96 | values.notionLastUpdate?.let { 97 | propertyList.date("notion_update_time", DateOrDateRange(DateTime(Timestamp(it.time)), null)) 98 | } 99 | 100 | notionClient.pages.updatePage( 101 | id = values.pageId, 102 | properties = propertyList 103 | ) 104 | 105 | syncTable.first { it.pageId == values.pageId }.apply { 106 | notionId = values.notionId 107 | todoistId = values.todoistId 108 | } 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/main/kotlin/com/maaxgr/todoistnotionsync/SyncApplication.kt: -------------------------------------------------------------------------------- 1 | package com.maaxgr.todoistnotionsync 2 | 3 | import com.maaxgr.todoistnotionsync.interfaces.notionconfigdatabase.NotionConfigDatabase 4 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.NotionRepo 5 | import com.maaxgr.todoistnotionsync.interfaces.notionrepo.databasequery.Result 6 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.AddedTodoistItemIntegrator 7 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.CompletedTodoistItemIntegrator 8 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.UncompletedTodoistItemIntegrator 9 | import com.maaxgr.todoistnotionsync.interfaces.sync.integrator.UpdatedTodoistItemIntegrator 10 | import com.maaxgr.todoistnotionsync.interfaces.synctable.SyncTable 11 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.TodoistRepo 12 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.getacitivty.Event 13 | import com.maaxgr.todoistnotionsync.interfaces.todoistrepo.entities.sync.Item 14 | import com.maaxgr.todoistnotionsync.todoistmanager.TodoistManager 15 | import org.jraf.klibnotion.model.page.Page 16 | import org.koin.core.component.KoinComponent 17 | import org.koin.core.component.inject 18 | import java.text.SimpleDateFormat 19 | import java.util.* 20 | 21 | class SyncApplication : KoinComponent { 22 | 23 | private val notionRepo: NotionRepo by inject() 24 | private val todoistRepo: TodoistRepo by inject() 25 | 26 | private val syncTable: SyncTable by inject() 27 | 28 | private val notionConfigDatabase: NotionConfigDatabase by inject() 29 | 30 | private val todoistManager = TodoistManager() 31 | 32 | private var notionEntries = listOf<NotionRepo.Entry>() 33 | 34 | suspend fun init() { 35 | syncTable.reloadSyncTable() 36 | 37 | //val syncTable = notionRepo.getSyncTable() 38 | notionEntries = notionRepo.getDatabaseEntries("") 39 | // val todoistEntries = todoistRepo.getTodoistEntries() 40 | 41 | integrateTodoistUpdates() 42 | 43 | //syncEntries(notionEntries, todoistEntries) 44 | 45 | // val notionIdMapping = notionEntries.associateBy { it.id } 46 | // val todoistMappingByNotionId = todoistEntries.associateBy { it.description } 47 | // 48 | // notionEntries.forEach { notionEntry -> 49 | // val notionId = notionEntry.id 50 | // val notionTaskName = notionEntry.properties.Name.title.first().plain_text 51 | // 52 | // val todoistEntry = todoistMappingByNotionId[notionId] 53 | // 54 | // 55 | // if (todoistEntry == null) { 56 | // println("Create task in todoist: $notionTaskName") 57 | // todoistRepo.createEntry(notionTaskName) 58 | // } else { 59 | // if (notionTaskName != todoistEntry.content) { 60 | // println("Update task in todoist: $notionTaskName") 61 | // todoistRepo.updateContent(todoistEntry.id, notionTaskName) 62 | // } 63 | // 64 | // } 65 | // 66 | // } 67 | } 68 | 69 | private suspend fun integrateTodoistUpdates() { 70 | val updatesToIntegrate = getTodoistUpdatesToIntegrate() 71 | println("Updates to integrate: ${updatesToIntegrate.size}") 72 | 73 | for (update in updatesToIntegrate) { 74 | integrateTodoistUpdate(update) 75 | } 76 | 77 | if (updatesToIntegrate.isNotEmpty()) { 78 | notionConfigDatabase.setValue("todoist_last_activity_id", updatesToIntegrate.maxOf { it.id }.toString()) 79 | } 80 | } 81 | 82 | private suspend fun integrateTodoistUpdate(update: Event) { 83 | if (update.object_type != "item") { 84 | println("Ignore update ${update.id} because eventtype ${update.object_type}") 85 | return 86 | } 87 | 88 | when (update.event_type) { 89 | "updated" -> UpdatedTodoistItemIntegrator(update, notionEntries).integrate() 90 | "completed" -> CompletedTodoistItemIntegrator(update, notionEntries).integrate() 91 | "uncompleted" -> UncompletedTodoistItemIntegrator(update, notionEntries).integrate() 92 | "added" -> AddedTodoistItemIntegrator(update, notionEntries).integrate() 93 | else -> println("No integration available for event type '${update.event_type}'") 94 | } 95 | } 96 | 97 | private suspend fun getTodoistUpdatesToIntegrate(): List<Event> { 98 | val todoistLastActivityId = notionConfigDatabase.getValue("todoist_last_activity_id")?.toLongOrNull() ?: 0L 99 | return todoistManager.getUpdatesToProcess(todoistLastActivityId) 100 | } 101 | 102 | 103 | private suspend fun syncEntries(notionEntries: List<Result>, todoistEntries: List<Item>) { 104 | val notionIdMapping = notionEntries.associateBy { it.id } 105 | val todoistIdMapping = todoistEntries.associateBy { it.id } 106 | 107 | val syncTableEntry = syncTable.getSyncTable() 108 | 109 | // iterate notion entries 110 | notionEntries.forEach { notionEntry -> 111 | 112 | val syncEntry = syncTableEntry.firstOrNull { it.notionId == notionEntry.id } 113 | 114 | val text = notionEntry.properties.Name.title.firstOrNull()?.plain_text ?: "null" 115 | 116 | val todoistAddResponse = todoistRepo.createEntry(text) 117 | 118 | val todoistLastUpdate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").apply { timeZone = TimeZone.getTimeZone("GMT") }.parse(todoistAddResponse.created) 119 | val notionLastUpdate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").apply { timeZone = TimeZone.getTimeZone("GMT") }.parse(notionEntry.last_edited_time) 120 | 121 | if (syncEntry == null) { 122 | syncTable.addSyncEntry( 123 | SyncTable.AddSyncTableEntry( 124 | notionId = notionEntry.id, 125 | todoistId = todoistAddResponse.id, 126 | todoistLastUpdate = todoistLastUpdate, 127 | notionLastUpdate = notionLastUpdate 128 | ) 129 | ) 130 | } else { 131 | 132 | val todoistEntry = todoistIdMapping[syncEntry.todoistId] 133 | 134 | if (todoistEntry == null) { 135 | syncTable.addSyncEntry( 136 | SyncTable.AddSyncTableEntry( 137 | notionId = notionEntry.id, 138 | todoistId = todoistAddResponse.id, 139 | todoistLastUpdate = todoistLastUpdate, 140 | notionLastUpdate = notionLastUpdate 141 | ) 142 | ) 143 | } else { 144 | 145 | if (syncEntry.todoistLastUpdate == null || notionLastUpdate.time > syncEntry.todoistLastUpdate!!.time) { 146 | val updatedEntry = todoistRepo.updateContent(syncEntry.todoistId, text) 147 | //val updatedTimestamp = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").apply { timeZone = TimeZone.getTimeZone("GMT") }.parse(updatedEntry) 148 | 149 | syncTable.updateSyncEntry( 150 | syncEntry.copy() 151 | ) 152 | } 153 | 154 | } 155 | } 156 | } 157 | 158 | 159 | } 160 | 161 | } 162 | --------------------------------------------------------------------------------