From 2a5ffa54c8f0b49c2c8bfb7f4fe5d5d9290f35fb Mon Sep 17 00:00:00 2001 From: GerardPaligot Date: Fri, 5 Apr 2024 23:04:05 +0200 Subject: [PATCH] feat(backend): merge existing data between OpenPlanner and the project. --- .../org/gdglille/devfest/backend/Server.kt | 11 +- .../devfest/backend/categories/CategoryDao.kt | 18 +- .../devfest/backend/events/EventRepository.kt | 48 ----- .../devfest/backend/events/EventRouting.kt | 17 +- .../devfest/backend/formats/FormatDao.kt | 5 + .../internals/helpers/database/Database.kt | 2 + .../helpers/database/FirestoreDatabase.kt | 202 +++++++++++------- .../backend/schedulers/ScheduleItemDao.kt | 5 + .../devfest/backend/speakers/SpeakerDao.kt | 5 + .../gdglille/devfest/backend/talks/TalkDao.kt | 5 + .../parties/openplanner/OpenPlannerMappers.kt | 55 ++++- .../openplanner/OpenPlannerRepository.kt | 149 +++++++++++++ .../parties/openplanner/OpenPlannerRouting.kt | 34 +++ 13 files changed, 405 insertions(+), 151 deletions(-) create mode 100644 backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRepository.kt create mode 100644 backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRouting.kt diff --git a/backend/src/main/java/org/gdglille/devfest/backend/Server.kt b/backend/src/main/java/org/gdglille/devfest/backend/Server.kt index 0efc00086..fe8383ac1 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/Server.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/Server.kt @@ -57,6 +57,7 @@ import org.gdglille.devfest.backend.third.parties.conferencehall.ConferenceHallA import org.gdglille.devfest.backend.third.parties.conferencehall.registerConferenceHallRoutes import org.gdglille.devfest.backend.third.parties.geocode.GeocodeApi import org.gdglille.devfest.backend.third.parties.openplanner.OpenPlannerApi +import org.gdglille.devfest.backend.third.parties.openplanner.registerOpenPlannerRoutes import org.gdglille.devfest.backend.third.parties.welovedevs.WeLoveDevsApi import org.gdglille.devfest.backend.third.parties.welovedevs.registerWLDRoutes import org.gdglille.devfest.models.inputs.Validator @@ -160,7 +161,6 @@ fun main() { routing { registerEventRoutes( geocodeApi, - openPlannerApi, eventDao, speakerDao, qAndADao, @@ -209,6 +209,15 @@ fun main() { categoryDao, formatDao ) + registerOpenPlannerRoutes( + openPlannerApi, + eventDao, + speakerDao, + talkDao, + categoryDao, + formatDao, + scheduleItemDao + ) registerWLDRoutes(wldApi, eventDao, partnerDao, jobDao) } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/categories/CategoryDao.kt b/backend/src/main/java/org/gdglille/devfest/backend/categories/CategoryDao.kt index 91ed37632..666068f8c 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/categories/CategoryDao.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/categories/CategoryDao.kt @@ -31,20 +31,8 @@ class CategoryDao(private val database: Database) { ) } - suspend fun createIfNotExist(eventId: String, item: CategoryDb) { - if (item.id == null) { - database.insert( - eventId = eventId, - collectionName = CollectionName - ) { item.copy(id = it) } - } else { - val existing = get(eventId, item.id) - if (existing == null) { - database.insert( - eventId = eventId, - collectionName = CollectionName - ) { item.copy(id = it) } - } - } + suspend fun deleteDiff(eventId: String, ids: List) { + val diff = database.diff(eventId, CollectionName, ids) + database.deleteAll(eventId, CollectionName, diff) } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/events/EventRepository.kt b/backend/src/main/java/org/gdglille/devfest/backend/events/EventRepository.kt index 4b18817f7..a2ab83500 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/events/EventRepository.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/events/EventRepository.kt @@ -20,10 +20,6 @@ import org.gdglille.devfest.backend.talks.TalkDao import org.gdglille.devfest.backend.talks.convertToModel import org.gdglille.devfest.backend.third.parties.geocode.GeocodeApi import org.gdglille.devfest.backend.third.parties.geocode.convertToDb -import org.gdglille.devfest.backend.third.parties.openplanner.OpenPlannerApi -import org.gdglille.devfest.backend.third.parties.openplanner.convertToDb -import org.gdglille.devfest.backend.third.parties.openplanner.convertToScheduleDb -import org.gdglille.devfest.backend.third.parties.openplanner.convertToTalkDb import org.gdglille.devfest.models.Agenda import org.gdglille.devfest.models.CreatedEvent import org.gdglille.devfest.models.Event @@ -39,7 +35,6 @@ import java.time.LocalDateTime @Suppress("LongParameterList") class EventRepository( private val geocodeApi: GeocodeApi, - private val openPlannerApi: OpenPlannerApi, private val eventDao: EventDao, private val speakerDao: SpeakerDao, private val qAndADao: QAndADao, @@ -150,47 +145,4 @@ class EventRepository( }.awaitAll().associate { it }.toSortedMap() return@coroutineScope Agenda(talks = schedules) } - - suspend fun openPlanner(eventId: String, apiKey: String) = - coroutineScope { - val event = eventDao.getVerified(eventId, apiKey) - val config = event.openPlannerConfig - ?: throw NotAcceptableException("OpenPlanner config not initialized") - val openPlanner = openPlannerApi.fetchPrivateJson(config.eventId, config.privateId) - openPlanner.event.categories - .map { async { categoryDao.createIfNotExist(eventId, it.convertToDb()) } } - .awaitAll() - openPlanner.event.formats - .map { async { formatDao.createOrUpdate(eventId, it.convertToDb()) } } - .awaitAll() - val allSpeakers = openPlanner.sessions - .map { it.speakerIds }.flatten() - openPlanner.speakers - .filter { allSpeakers.contains(it.id) } - .map { async { speakerDao.createOrUpdate(eventId, it.convertToDb()) } } - .awaitAll() - openPlanner.sessions - .map { async { talkDao.createOrUpdate(eventId, it.convertToTalkDb()) } } - val trackIds = openPlanner.event.tracks.map { it.id } - openPlanner.sessions - .filter { it.trackId != null && it.dateStart != null && it.dateEnd != null } - .groupBy { it.dateStart } - .map { - async { - it.value - .sortedWith { sessionA, sessionB -> - trackIds.indexOf(sessionA.trackId) - .compareTo(trackIds.indexOf(sessionB.trackId)) - } - .forEachIndexed { index, sessionOP -> - scheduleItemDao.createOrUpdate( - eventId, - sessionOP.convertToScheduleDb(index, openPlanner.event.tracks) - ) - } - } - } - .awaitAll() - eventDao.updateAgendaUpdatedAt(event) - } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/events/EventRouting.kt b/backend/src/main/java/org/gdglille/devfest/backend/events/EventRouting.kt index 0b9f108b3..dca13b7a6 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/events/EventRouting.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/events/EventRouting.kt @@ -17,7 +17,10 @@ import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.put -import org.gdglille.devfest.backend.NotAuthorized +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime import org.gdglille.devfest.backend.NotFoundException import org.gdglille.devfest.backend.categories.CategoryDao import org.gdglille.devfest.backend.formats.FormatDao @@ -28,22 +31,16 @@ import org.gdglille.devfest.backend.schedulers.ScheduleItemDao import org.gdglille.devfest.backend.speakers.SpeakerDao import org.gdglille.devfest.backend.talks.TalkDao import org.gdglille.devfest.backend.third.parties.geocode.GeocodeApi -import org.gdglille.devfest.backend.third.parties.openplanner.OpenPlannerApi import org.gdglille.devfest.backend.version import org.gdglille.devfest.models.inputs.CoCInput import org.gdglille.devfest.models.inputs.CreatingEventInput import org.gdglille.devfest.models.inputs.EventInput import org.gdglille.devfest.models.inputs.FeaturesActivatedInput import org.gdglille.devfest.models.inputs.LunchMenuInput -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZonedDateTime @Suppress("LongMethod", "LongParameterList", "MagicNumber") fun Route.registerEventRoutes( geocodeApi: GeocodeApi, - openPlannerApi: OpenPlannerApi, eventDao: EventDao, speakerDao: SpeakerDao, qAndADao: QAndADao, @@ -55,7 +52,6 @@ fun Route.registerEventRoutes( ) { val repository = EventRepository( geocodeApi, - openPlannerApi, eventDao, speakerDao, qAndADao, @@ -155,9 +151,4 @@ fun Route.registerEventRoutes( val eventId = call.parameters["eventId"]!! call.respond(HttpStatusCode.OK, repositoryV2.openFeedback(eventId)) } - post("/events/{eventId}/openplanner") { - val eventId = call.parameters["eventId"]!! - val apiKey = call.request.queryParameters["api_key"] ?: throw NotAuthorized - call.respond(HttpStatusCode.Created, repository.openPlanner(eventId, apiKey)) - } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/formats/FormatDao.kt b/backend/src/main/java/org/gdglille/devfest/backend/formats/FormatDao.kt index ba4e284b5..add701371 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/formats/FormatDao.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/formats/FormatDao.kt @@ -30,4 +30,9 @@ class FormatDao(private val database: Database) { item = item ) } + + suspend fun deleteDiff(eventId: String, ids: List) { + val diff = database.diff(eventId, CollectionName, ids) + database.deleteAll(eventId, CollectionName, diff) + } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/Database.kt b/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/Database.kt index 9238ef96c..42cdaa0c8 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/Database.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/Database.kt @@ -19,6 +19,8 @@ interface Database { suspend fun update(eventId: String, collectionName: String, id: String, item: T) suspend fun delete(eventId: String, collectionName: String, id: String) suspend fun delete(eventId: String, collectionName: String) + suspend fun deleteAll(eventId: String, collectionName: String, ids: List) + suspend fun diff(eventId: String, collectionName: String, ids: List): List object Factory { fun create(firestore: Firestore, projectName: String): Database = FirestoreDatabase(firestore, projectName) diff --git a/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/FirestoreDatabase.kt b/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/FirestoreDatabase.kt index 8d9e33369..157b2014f 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/FirestoreDatabase.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/internals/helpers/database/FirestoreDatabase.kt @@ -10,120 +10,176 @@ import kotlinx.coroutines.withContext import kotlin.reflect.KClass import kotlin.reflect.full.memberProperties -class FirestoreDatabase(private val firestore: Firestore, private val projectName: String) : Database { - override suspend fun count(eventId: String, collectionName: String): Long = withContext(Dispatchers.IO) { - firestore.collection(projectName).document(eventId).collection(collectionName).count().get().get().count - } - - override suspend fun get(eventId: String, collectionName: String, id: String, clazz: KClass): T? = +class FirestoreDatabase(private val firestore: Firestore, private val projectName: String) : + Database { + override suspend fun count(eventId: String, collectionName: String): Long = withContext(Dispatchers.IO) { firestore .collection(projectName) .document(eventId) .collection(collectionName) - .document(id) + .count() .get() .get() - .toObject(clazz.java) + .count } - override suspend fun getAll(eventId: String, collectionName: String, clazz: KClass): List = - withContext(Dispatchers.IO) { - firestore - .collection(projectName) - .document(eventId) - .collection(collectionName) - .listDocuments() - .map { it.get().get().toObject(clazz.java)!! } - } + override suspend fun get( + eventId: String, + collectionName: String, + id: String, + clazz: KClass + ): T? = withContext(Dispatchers.IO) { + firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + .document(id) + .get() + .get() + .toObject(clazz.java) + } + + override suspend fun getAll( + eventId: String, + collectionName: String, + clazz: KClass + ): List = withContext(Dispatchers.IO) { + firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + .listDocuments() + .map { it.get().get().toObject(clazz.java)!! } + } override suspend fun query( eventId: String, collectionName: String, clazz: KClass, vararg ops: WhereOperation - ): List = - withContext(Dispatchers.IO) { - val collRef = firestore - .collection(projectName) - .document(eventId) - .collection(collectionName) - var query: Query? = null - ops.forEach { - val requester = if (query == null) collRef else query!! - query = when (it) { - is WhereOperation.WhereEquals<*> -> requester.whereEqualTo(it.left, it.right) - is WhereOperation.WhereNotEquals<*> -> requester.whereNotEqualTo(it.left, it.right) - is WhereOperation.WhereIn<*> -> requester.whereIn(it.left, it.right) - } + ): List = withContext(Dispatchers.IO) { + val collRef = firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + var query: Query? = null + ops.forEach { + val requester = if (query == null) collRef else query!! + query = when (it) { + is WhereOperation.WhereEquals<*> -> requester.whereEqualTo(it.left, it.right) + is WhereOperation.WhereNotEquals<*> -> requester.whereNotEqualTo(it.left, it.right) + is WhereOperation.WhereIn<*> -> requester.whereIn(it.left, it.right) } - if (query == null) error("You can't create a query without any where condition") - query!! - .get() - .get() - .documents - .map { it.toObject(clazz.java) } } + if (query == null) error("You can't create a query without any where condition") + query!! + .get() + .get() + .documents + .map { it.toObject(clazz.java) } + } - override suspend fun insert(eventId: String, collectionName: String, id: String, item: T) = - withContext(Dispatchers.IO) { - firestore - .collection(projectName) - .document(eventId) - .collection(collectionName) - .document(id) - .set(item) - .get() - Unit - } + override suspend fun insert( + eventId: String, + collectionName: String, + id: String, + item: T + ) = withContext(Dispatchers.IO) { + firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + .document(id) + .set(item) + .get() + Unit + } override suspend fun insert( eventId: String, collectionName: String, transform: (id: String) -> T - ): String = + ): String = withContext(Dispatchers.IO) { + val docRef = firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + .document() + val item = transform(docRef.id) + docRef.set(item).get() + return@withContext docRef.id + } + + override suspend fun update( + eventId: String, + collectionName: String, + id: String, + item: T + ) = withContext(Dispatchers.IO) { + val map = item::class.memberProperties.associate { it.name to it.getter.call(item) } + firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + .document(id) + .set(map, SetOptions.merge()) + .get() + Unit + } + + override suspend fun delete(eventId: String, collectionName: String, id: String) = withContext(Dispatchers.IO) { - val docRef = firestore + firestore .collection(projectName) .document(eventId) .collection(collectionName) - .document() - val item = transform(docRef.id) - docRef.set(item).get() - return@withContext docRef.id + .document(id) + .delete() + .get() + Unit } - override suspend fun update(eventId: String, collectionName: String, id: String, item: T) = + override suspend fun delete(eventId: String, collectionName: String) = withContext(Dispatchers.IO) { - val map = item::class.memberProperties.associate { it.name to it.getter.call(item) } - firestore + val documents = firestore .collection(projectName) .document(eventId) .collection(collectionName) - .document(id) - .set(map, SetOptions.merge()) - .get() + .listDocuments() + documents.map { async { it.delete() } }.awaitAll() Unit } - override suspend fun delete(eventId: String, collectionName: String, id: String) = withContext(Dispatchers.IO) { - firestore - .collection(projectName) - .document(eventId) - .collection(collectionName) - .document(id) - .delete() - .get() - Unit + override suspend fun deleteAll( + eventId: String, + collectionName: String, + ids: List + ): Unit = withContext(Dispatchers.IO) { + val batch = firestore.batch() + ids.forEach { + batch.delete( + firestore + .collection(projectName) + .document(eventId) + .collection(collectionName) + .document(it) + ) + } + batch.commit().get() } - override suspend fun delete(eventId: String, collectionName: String) = withContext(Dispatchers.IO) { - val documents = firestore + override suspend fun diff( + eventId: String, + collectionName: String, + ids: List + ): List = withContext(Dispatchers.IO) { + val saved = firestore .collection(projectName) .document(eventId) .collection(collectionName) .listDocuments() - documents.map { async { it.delete() } }.awaitAll() - Unit + .map { it.id } + saved - ids.toSet() } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/schedulers/ScheduleItemDao.kt b/backend/src/main/java/org/gdglille/devfest/backend/schedulers/ScheduleItemDao.kt index 80c490074..b891be915 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/schedulers/ScheduleItemDao.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/schedulers/ScheduleItemDao.kt @@ -34,4 +34,9 @@ class ScheduleItemDao(private val database: Database) { collectionName = CollectionName, id = id ) + + suspend fun deleteDiff(eventId: String, ids: List) { + val diff = database.diff(eventId, CollectionName, ids) + database.deleteAll(eventId, CollectionName, diff) + } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/speakers/SpeakerDao.kt b/backend/src/main/java/org/gdglille/devfest/backend/speakers/SpeakerDao.kt index 16a4bd771..1336a068d 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/speakers/SpeakerDao.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/speakers/SpeakerDao.kt @@ -79,4 +79,9 @@ class SpeakerDao(private val database: Database, private val storage: Storage) { filename = "$eventId/speakers/$id.png", content = content ) + + suspend fun deleteDiff(eventId: String, ids: List) { + val diff = database.diff(eventId, CollectionName, ids) + database.deleteAll(eventId, CollectionName, diff) + } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/talks/TalkDao.kt b/backend/src/main/java/org/gdglille/devfest/backend/talks/TalkDao.kt index 03e655962..705bdb477 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/talks/TalkDao.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/talks/TalkDao.kt @@ -60,4 +60,9 @@ class TalkDao(private val database: Database) { else database.update(eventId = eventId, collectionName = CollectionName, id = talk.id, item = talk) return@coroutineScope talk.id } + + suspend fun deleteDiff(eventId: String, ids: List) { + val diff = database.diff(eventId, CollectionName, ids) + database.deleteAll(eventId, CollectionName, diff) + } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerMappers.kt b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerMappers.kt index 4dc33008f..6686a34a9 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerMappers.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerMappers.kt @@ -9,16 +9,29 @@ import org.gdglille.devfest.backend.talks.TalkDb fun CategoryOP.convertToDb() = CategoryDb( id = id, name = name, - color = color, + color = "", icon = "" ) +fun CategoryDb.mergeWith(category: CategoryOP) = CategoryDb( + id = category.id, + name = if (this.name == category.name) this.name else category.name, + color = if (this.color != "") this.color else category.color, + icon = if (this.icon != "") this.icon else "" +) + fun FormatOP.convertToDb() = FormatDb( id = id, name = name, time = durationMinutes ) +fun FormatDb.mergeWith(formatOP: FormatOP) = FormatDb( + id = formatOP.id, + name = if (this.name == formatOP.name) this.name else formatOP.name, + time = if (this.time != 0) this.time else formatOP.durationMinutes +) + fun SpeakerOP.convertToDb(): SpeakerDb { val twitter = socials.find { it.name == "Twitter" }?.link val github = socials.find { it.name == "GitHub" }?.link @@ -33,9 +46,36 @@ fun SpeakerOP.convertToDb(): SpeakerDb { photoUrl = photoUrl ?: "", website = null, twitter = if (twitter?.contains("twitter.com") == true) twitter + else if (twitter == null) null else "https://twitter.com/$twitter", mastodon = null, github = if (github?.contains("github.com") == true) github + else if (github == null) null + else "https://github.com/$github", + linkedin = null + ) +} + +fun SpeakerDb.mergeWith(speakerOP: SpeakerOP): SpeakerDb { + val twitter = speakerOP.socials.find { it.name == "Twitter" }?.link + val github = speakerOP.socials.find { it.name == "GitHub" }?.link + return SpeakerDb( + id = speakerOP.id, + displayName = if (this.displayName == speakerOP.name) this.displayName else speakerOP.name, + pronouns = null, + bio = if (this.bio == speakerOP.bio) this.bio else speakerOP.bio ?: "", + email = if (this.email == speakerOP.email) this.email else speakerOP.email, + jobTitle = if (this.jobTitle == speakerOP.jobTitle) this.jobTitle else speakerOP.jobTitle, + company = if (this.company == speakerOP.company) this.company else speakerOP.company, + photoUrl = if (this.photoUrl == speakerOP.photoUrl) this.photoUrl else speakerOP.photoUrl ?: "", + website = null, + twitter = if (this.twitter == twitter) this.twitter + else if (twitter?.contains("twitter.com") == true) twitter + else if (twitter == null) null + else "https://twitter.com/$twitter", + github = if (this.github == github) this.github + else if (github?.contains("github.com") == true) github + else if (github == null) null else "https://github.com/$github", linkedin = null ) @@ -54,6 +94,19 @@ fun SessionOP.convertToTalkDb() = TalkDb( linkReplay = null ) +fun TalkDb.mergeWith(sessionOP: SessionOP) = TalkDb( + id = sessionOP.id, + title = if (title == sessionOP.title) title else sessionOP.title, + level = if (level == sessionOP.level) level else sessionOP.level, + abstract = if (abstract == sessionOP.abstract) abstract else sessionOP.abstract, + category = if (category == sessionOP.categoryId) category else sessionOP.categoryId, + format = if (format == sessionOP.formatId) format else sessionOP.formatId, + language = if (language == sessionOP.language) language else sessionOP.language, + speakerIds = if (speakerIds == sessionOP.speakerIds) speakerIds else sessionOP.speakerIds, + linkSlides = linkSlides, + linkReplay = linkReplay +) + fun SessionOP.convertToScheduleDb(order: Int, tracks: List) = ScheduleDb( order = order, startTime = dateStart?.split("+")?.first() diff --git a/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRepository.kt b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRepository.kt new file mode 100644 index 000000000..5fc32e8db --- /dev/null +++ b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRepository.kt @@ -0,0 +1,149 @@ +package org.gdglille.devfest.backend.third.parties.openplanner + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import org.gdglille.devfest.backend.NotAcceptableException +import org.gdglille.devfest.backend.categories.CategoryDao +import org.gdglille.devfest.backend.categories.CategoryDb +import org.gdglille.devfest.backend.events.EventDao +import org.gdglille.devfest.backend.formats.FormatDao +import org.gdglille.devfest.backend.formats.FormatDb +import org.gdglille.devfest.backend.schedulers.ScheduleDb +import org.gdglille.devfest.backend.schedulers.ScheduleItemDao +import org.gdglille.devfest.backend.speakers.SpeakerDao +import org.gdglille.devfest.backend.speakers.SpeakerDb +import org.gdglille.devfest.backend.talks.TalkDao +import org.gdglille.devfest.backend.talks.TalkDb + +class OpenPlannerRepository( + private val openPlannerApi: OpenPlannerApi, + private val eventDao: EventDao, + private val speakerDao: SpeakerDao, + private val talkDao: TalkDao, + private val categoryDao: CategoryDao, + private val formatDao: FormatDao, + private val scheduleItemDao: ScheduleItemDao +) { + suspend fun update(eventId: String, apiKey: String) = coroutineScope { + val event = eventDao.getVerified(eventId, apiKey) + val config = event.openPlannerConfig + ?: throw NotAcceptableException("OpenPlanner config not initialized") + val openPlanner = openPlannerApi.fetchPrivateJson(config.eventId, config.privateId) + val categories = openPlanner.event.categories + .map { async { createOrMergeCategory(eventId, it) } } + .awaitAll() + val formats = openPlanner.event.formats + .map { async { createOrMergeFormat(eventId, it) } } + .awaitAll() + val allSpeakers = openPlanner.sessions + .map { it.speakerIds }.flatten() + val speakers = openPlanner.speakers + .filter { allSpeakers.contains(it.id) } + .map { async { createOrMergeSpeaker(eventId, it) } } + .awaitAll() + val talks = openPlanner.sessions + .map { async { createOrMergeTalks(eventId, it) } } + .awaitAll() + val trackIds = openPlanner.event.tracks.map { it.id } + val schedules = openPlanner.sessions + .filter { it.trackId != null && it.dateStart != null && it.dateEnd != null } + .groupBy { it.dateStart } + .map { + async { + it.value + .sortedWith { sessionA, sessionB -> + trackIds.indexOf(sessionA.trackId) + .compareTo(trackIds.indexOf(sessionB.trackId)) + } + .mapIndexed { index, sessionOP -> + createOrMergeSchedule( + eventId, + index, + sessionOP, + openPlanner.event.tracks + ) + } + } + } + .awaitAll() + .flatten() + clean(eventId, categories, formats, speakers, talks, schedules) + eventDao.updateAgendaUpdatedAt(event) + } + + private suspend fun clean( + eventId: String, + categories: List, + formats: List, + speakers: List, + talks: List, + schedules: List + ) = coroutineScope { + categoryDao.deleteDiff(eventId, categories.map { it.id!! }) + formatDao.deleteDiff(eventId, formats.map { it.id!! }) + speakerDao.deleteDiff(eventId, speakers.map { it.id }) + talkDao.deleteDiff(eventId, talks.map { it.id }) + scheduleItemDao.deleteDiff(eventId, schedules.map { it.id }) + } + + private suspend fun createOrMergeCategory(eventId: String, category: CategoryOP): CategoryDb { + val existing = categoryDao.get(eventId, category.id) + return if (existing == null) { + val item = category.convertToDb() + categoryDao.createOrUpdate(eventId, item) + item + } else { + val item = existing.mergeWith(category) + categoryDao.createOrUpdate(eventId, item) + item + } + } + + private suspend fun createOrMergeFormat(eventId: String, format: FormatOP): FormatDb { + val existing = formatDao.get(eventId, format.id) + return if (existing == null) { + val item = format.convertToDb() + formatDao.createOrUpdate(eventId, item) + item + } else { + val item = existing.mergeWith(format) + formatDao.createOrUpdate(eventId, item) + item + } + } + + private suspend fun createOrMergeSpeaker(eventId: String, speaker: SpeakerOP): SpeakerDb { + val existing = speakerDao.get(eventId, speaker.id) + return if (existing == null) { + val item = speaker.convertToDb() + speakerDao.createOrUpdate(eventId, item) + item + } else { + val item = existing.mergeWith(speaker) + speakerDao.createOrUpdate(eventId, item) + item + } + } + + private suspend fun createOrMergeTalks(eventId: String, session: SessionOP): TalkDb { + val existing = talkDao.get(eventId, session.id) + return if (existing == null) { + val item = session.convertToTalkDb() + talkDao.createOrUpdate(eventId, item) + item + } else { + val item = existing.mergeWith(session) + talkDao.createOrUpdate(eventId, item) + item + } + } + + private suspend fun createOrMergeSchedule( + eventId: String, order: Int, session: SessionOP, tracks: List + ): ScheduleDb { + val item = session.convertToScheduleDb(order, tracks) + scheduleItemDao.createOrUpdate(eventId, item) + return item + } +} diff --git a/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRouting.kt b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRouting.kt new file mode 100644 index 000000000..280fae065 --- /dev/null +++ b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerRouting.kt @@ -0,0 +1,34 @@ +package org.gdglille.devfest.backend.third.parties.openplanner + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.post +import org.gdglille.devfest.backend.NotAuthorized +import org.gdglille.devfest.backend.categories.CategoryDao +import org.gdglille.devfest.backend.events.EventDao +import org.gdglille.devfest.backend.formats.FormatDao +import org.gdglille.devfest.backend.schedulers.ScheduleItemDao +import org.gdglille.devfest.backend.speakers.SpeakerDao +import org.gdglille.devfest.backend.talks.TalkDao + +fun Route.registerOpenPlannerRoutes( + openPlannerApi: OpenPlannerApi, + eventDao: EventDao, + speakerDao: SpeakerDao, + talkDao: TalkDao, + categoryDao: CategoryDao, + formatDao: FormatDao, + scheduleItemDao: ScheduleItemDao, +) { + val repository = OpenPlannerRepository( + openPlannerApi, eventDao, speakerDao, talkDao, categoryDao, formatDao, scheduleItemDao + ) + + post("openplanner/webhook") { + val eventId = call.parameters["eventId"]!! + val apiKey = call.request.queryParameters["api_key"] ?: throw NotAuthorized + call.respond(HttpStatusCode.Created, repository.update(eventId, apiKey)) + } +}