diff --git a/androidApp/src/main/java/org/gdglille/devfest/android/MainActivity.kt b/androidApp/src/main/java/org/gdglille/devfest/android/MainActivity.kt index cf06450bd..a546b7c66 100644 --- a/androidApp/src/main/java/org/gdglille/devfest/android/MainActivity.kt +++ b/androidApp/src/main/java/org/gdglille/devfest/android/MainActivity.kt @@ -80,7 +80,7 @@ class MainActivity : ComponentActivity() { talkDao = TalkDao(db, platform), eventDao = EventDao(db, settings), partnerDao = PartnerDao(db = db, platform = platform), - featuresDao = FeaturesActivatedDao(db), + featuresDao = FeaturesActivatedDao(db, settings), qrCodeGenerator = QrCodeGeneratorAndroid() ) val userRepository = UserRepository.Factory.create( 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 6687e3c7d..82e0e32c5 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/Server.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/Server.kt @@ -6,6 +6,8 @@ import com.google.cloud.secretmanager.v1.SecretManagerServiceClient import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings import com.google.cloud.storage.StorageOptions import io.ktor.http.HeaderValue +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.ApplicationCall @@ -46,6 +48,7 @@ import org.gdglille.devfest.backend.third.parties.billetweb.registerBilletWebRou import org.gdglille.devfest.backend.third.parties.conferencehall.ConferenceHallApi 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.welovedevs.WeLoveDevsApi import org.gdglille.devfest.backend.third.parties.welovedevs.registerWLDRoutes import org.gdglille.devfest.models.inputs.Validator @@ -101,10 +104,19 @@ fun main() { apiKey = secret["GEOCODE_API_KEY"], enableNetworkLogs = true ) + val openPlannerApi = OpenPlannerApi.Factory.create(enableNetworkLogs = true) val conferenceHallApi = ConferenceHallApi.Factory.create(enableNetworkLogs = true) val imageTranscoder = TranscoderImage() embeddedServer(Netty, PORT) { install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Delete) + allowHeader(HttpHeaders.AccessControlAllowOrigin) + allowHeader(HttpHeaders.ContentType) + allowHeader(HttpHeaders.Authorization) anyHost() } install(ContentNegotiation) { @@ -128,6 +140,7 @@ fun main() { routing { registerEventRoutes( geocodeApi, + openPlannerApi, eventDao, speakerDao, qAndADao, diff --git a/backend/src/main/java/org/gdglille/devfest/backend/events/EventDb.kt b/backend/src/main/java/org/gdglille/devfest/backend/events/EventDb.kt index c241e6f27..cf6fc340f 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/events/EventDb.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/events/EventDb.kt @@ -29,6 +29,11 @@ data class ConferenceHallConfigurationDb( val apiKey: String = "" ) +data class OpenPlannerConfigurationDb( + val eventId: String = "", + val privateId: String = "" +) + data class BilletWebConfigurationDb( val eventId: String = "", val userId: String = "", @@ -45,6 +50,7 @@ data class EventDb( val year: String = "", val openFeedbackId: String? = null, val conferenceHallConfig: ConferenceHallConfigurationDb? = null, + val openPlannerConfig: OpenPlannerConfigurationDb? = null, val billetWebConfig: BilletWebConfigurationDb? = null, val wldConfig: WldConfigurationDb? = null, val apiKey: String = "", diff --git a/backend/src/main/java/org/gdglille/devfest/backend/events/EventMappers.kt b/backend/src/main/java/org/gdglille/devfest/backend/events/EventMappers.kt index a0cd6205a..942b8524a 100644 --- a/backend/src/main/java/org/gdglille/devfest/backend/events/EventMappers.kt +++ b/backend/src/main/java/org/gdglille/devfest/backend/events/EventMappers.kt @@ -17,6 +17,7 @@ import org.gdglille.devfest.models.inputs.ConferenceHallConfigInput import org.gdglille.devfest.models.inputs.CreatingEventInput import org.gdglille.devfest.models.inputs.EventInput import org.gdglille.devfest.models.inputs.LunchMenuInput +import org.gdglille.devfest.models.inputs.OpenPlannerConfigInput import org.gdglille.devfest.models.inputs.WldConfigInput import java.util.UUID @@ -128,6 +129,11 @@ fun ConferenceHallConfigInput.convertToDb() = ConferenceHallConfigurationDb( apiKey = apiKey ) +fun OpenPlannerConfigInput.convertToDb() = OpenPlannerConfigurationDb( + eventId = eventId, + privateId = privateId +) + fun BilletWebConfigInput.convertToDb() = BilletWebConfigurationDb( eventId = eventId, userId = userId, @@ -148,6 +154,7 @@ fun EventInput.convertToDb(event: EventDb, addressDb: AddressDb) = EventDb( address = addressDb, openFeedbackId = openFeedbackId ?: event.openFeedbackId, conferenceHallConfig = this.conferenceHallConfigInput?.convertToDb(), + openPlannerConfig = this.openPlannerConfigInput?.convertToDb(), billetWebConfig = this.billetWebConfig?.convertToDb(), wldConfig = this.wldConfig?.convertToDb(), menus = event.menus, 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 a2ab83500..411990ae2 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,6 +20,10 @@ 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 @@ -35,6 +39,7 @@ 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, @@ -145,4 +150,38 @@ 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.createOrUpdate(eventId, it.convertToDb()) } } + .awaitAll() + openPlanner.event.formats + .map { async { formatDao.createOrUpdate(eventId, it.convertToDb()) } } + .awaitAll() + openPlanner.speakers + .map { async { speakerDao.createOrUpdate(eventId, it.convertToDb()) } } + .awaitAll() + openPlanner.sessions + .map { async { talkDao.createOrUpdate(eventId, it.convertToTalkDb()) } } + openPlanner.sessions + .filter { it.trackId != null && it.dateStart != null && it.dateEnd != null } + .groupBy { it.dateStart } + .map { + async { + it.value.forEachIndexed { index, sessionOP -> + scheduleItemDao.createOrUpdate( + eventId, + sessionOP.convertToScheduleDb(index) + ) + } + } + } + .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 0e32fe451..26fe26f4d 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,6 +17,7 @@ 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 org.gdglille.devfest.backend.NotFoundException import org.gdglille.devfest.backend.categories.CategoryDao import org.gdglille.devfest.backend.formats.FormatDao @@ -27,6 +28,7 @@ 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 @@ -41,6 +43,7 @@ import java.time.ZonedDateTime @Suppress("LongMethod", "LongParameterList", "MagicNumber") fun Route.registerEventRoutes( geocodeApi: GeocodeApi, + openPlannerApi: OpenPlannerApi, eventDao: EventDao, speakerDao: SpeakerDao, qAndADao: QAndADao, @@ -52,6 +55,7 @@ fun Route.registerEventRoutes( ) { val repository = EventRepository( geocodeApi, + openPlannerApi, eventDao, speakerDao, qAndADao, @@ -151,4 +155,9 @@ 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.parameters["api_key"] ?: throw NotAuthorized + call.respond(HttpStatusCode.Created, repository.openPlanner(eventId, apiKey)) + } } diff --git a/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerApi.kt b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerApi.kt new file mode 100644 index 000000000..fe903816f --- /dev/null +++ b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerApi.kt @@ -0,0 +1,47 @@ +package org.gdglille.devfest.backend.third.parties.openplanner + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.java.Java +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.DEFAULT +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.get +import io.ktor.serialization.kotlinx.json.json +import kotlinx.datetime.Clock +import kotlinx.serialization.json.Json + +class OpenPlannerApi( + private val client: HttpClient, + private val baseUrl: String = "https://storage.googleapis.com/conferencecenterr.appspot.com" +) { + suspend fun fetchPrivateJson(eventId: String, privateId: String): OpenPlanner = + client.get("$baseUrl/events/$eventId/$privateId.json?t=${Clock.System.now().epochSeconds}") + .body() + + object Factory { + fun create(enableNetworkLogs: Boolean): OpenPlannerApi = + OpenPlannerApi( + client = HttpClient(Java.create()) { + install(ContentNegotiation) { + json( + Json { + isLenient = true + ignoreUnknownKeys = true + } + ) + } + if (enableNetworkLogs) { + install( + Logging + ) { + logger = Logger.DEFAULT + level = LogLevel.INFO + } + } + } + ) + } +} 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 new file mode 100644 index 000000000..62c524447 --- /dev/null +++ b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerMappers.kt @@ -0,0 +1,64 @@ +package org.gdglille.devfest.backend.third.parties.openplanner + +import org.gdglille.devfest.backend.categories.CategoryDb +import org.gdglille.devfest.backend.formats.FormatDb +import org.gdglille.devfest.backend.schedulers.ScheduleDb +import org.gdglille.devfest.backend.speakers.SpeakerDb +import org.gdglille.devfest.backend.talks.TalkDb + +fun CategoryOP.convertToDb() = CategoryDb( + id = id, + name = name, + color = color, + icon = "" +) + +fun FormatOP.convertToDb() = FormatDb( + id = id, + name = name, + time = durationMinutes +) + +fun SpeakerOP.convertToDb(): SpeakerDb { + val twitter = socials.find { it.name == "Twitter" }?.link + val github = socials.find { it.name == "GitHub" }?.link + return SpeakerDb( + id = id, + displayName = name, + pronouns = null, + bio = bio ?: "", + jobTitle = jobTitle, + company = company, + photoUrl = photoUrl ?: "", + website = null, + twitter = if (twitter?.contains("twitter.com") == true) twitter + else "https://twitter.com/$twitter", + mastodon = null, + github = if (github?.contains("github.com") == true) github + else "https://github.com/$github", + linkedin = null + ) +} + +fun SessionOP.convertToTalkDb() = TalkDb( + id = id, + title = title, + level = level, + abstract = abstract, + category = categoryId, + format = formatId, + language = language, + speakerIds = speakerIds, + linkSlides = null, + linkReplay = null +) + +fun SessionOP.convertToScheduleDb(order: Int) = ScheduleDb( + order = order, + startTime = dateStart?.split("+")?.first() + ?: error("Can't schedule a talk without a start time"), + endTime = dateEnd?.split("+")?.first() + ?: error("Can't schedule a talk without a end time"), + room = trackId ?: error("Can't schedule a talk without a room"), + talkId = id +) diff --git a/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerNet.kt b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerNet.kt new file mode 100644 index 000000000..fc3e03c24 --- /dev/null +++ b/backend/src/main/java/org/gdglille/devfest/backend/third/parties/openplanner/OpenPlannerNet.kt @@ -0,0 +1,81 @@ +package org.gdglille.devfest.backend.third.parties.openplanner + +import kotlinx.serialization.Serializable + +@Serializable +data class OpenPlanner( + val generatedAt: String, + val event: EventOP, + val speakers: List, + val sessions: List +) + +@Serializable +data class EventOP( + val id: String, + val name: String, + val scheduleVisible: Boolean, + val dateStart: String, + val dateEnd: String, + val formats: List, + val categories: List, + val tracks: List +) + +@Serializable +data class FormatOP( + val id: String, + val name: String, + val description: String? = null, + val durationMinutes: Int +) + +@Serializable +data class CategoryOP( + val id: String, + val name: String, + val color: String +) + +@Serializable +data class TrackOP( + val id: String, + val name: String +) + +@Serializable +data class SpeakerOP( + val id: String, + val name: String, + val bio: String?, + val photoUrl: String?, + val email: String, + val phone: String?, + val company: String?, + val geolocation: String?, + val jobTitle: String?, + val socials: List +) + +@Serializable +data class SocialOP( + val link: String, + val icon: String, + val name: String +) + +@Serializable +data class SessionOP( + val id: String, + val title: String, + val abstract: String, + val dateStart: String? = null, + val dateEnd: String? = null, + val durationMinutes: Int, + val speakerIds: List, + val trackId: String?, + val language: String, + val level: String, + val formatId: String, + val categoryId: String, +) diff --git a/iosApp/iosApp/ViewModelFactory.swift b/iosApp/iosApp/ViewModelFactory.swift index 6131169c6..9d8355b72 100644 --- a/iosApp/iosApp/ViewModelFactory.swift +++ b/iosApp/iosApp/ViewModelFactory.swift @@ -32,7 +32,7 @@ class ViewModelFactory: ObservableObject { talkDao: TalkDao(db: db, platform: platform), eventDao: EventDao(db: db, settings: settings), partnerDao: PartnerDao(db: db, platform: platform), - featuresDao: FeaturesActivatedDao(db: db), + featuresDao: FeaturesActivatedDao(db: db, settings: settings), qrCodeGenerator: QrCodeGeneratoriOS() ) self.userRepository = UserRepositoryImpl( diff --git a/shared/core/src/commonMain/kotlin/org/gdglille/devfest/database/FeaturesActivatedDao.kt b/shared/core/src/commonMain/kotlin/org/gdglille/devfest/database/FeaturesActivatedDao.kt index 168a88bb1..acc1e2257 100644 --- a/shared/core/src/commonMain/kotlin/org/gdglille/devfest/database/FeaturesActivatedDao.kt +++ b/shared/core/src/commonMain/kotlin/org/gdglille/devfest/database/FeaturesActivatedDao.kt @@ -4,16 +4,36 @@ import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOneOrDefault import app.cash.sqldelight.coroutines.mapToOneOrNull +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.coroutines.getStringOrNullFlow import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flow import org.gdglille.devfest.db.Conferences4HallDatabase import org.gdglille.devfest.models.ui.ScaffoldConfigUi -class FeaturesActivatedDao(private val db: Conferences4HallDatabase) { - fun fetchFeatures(eventId: String): Flow = combine( +@OptIn(ExperimentalSettingsApi::class) +class FeaturesActivatedDao( + private val db: Conferences4HallDatabase, + private val settings: ObservableSettings +) { + @OptIn(ExperimentalCoroutinesApi::class) + fun fetchFeatures(): Flow = settings.getStringOrNullFlow("EVENT_ID") + .flatMapConcat { + if (it != null) { + fetchFeatures(it) + } else { + flow { ScaffoldConfigUi() } + } + } + + private fun fetchFeatures(eventId: String): Flow = combine( db.featuresActivatedQueries.selectFeatures(eventId).asFlow().mapToOneOrNull(Dispatchers.IO), db.userQueries.selectQrCode(eventId).asFlow().mapToOneOrNull(Dispatchers.IO), db.sessionQueries.selectDays(eventId).asFlow().mapToList(Dispatchers.IO), diff --git a/shared/core/src/commonMain/kotlin/org/gdglille/devfest/repositories/AgendaRepository.kt b/shared/core/src/commonMain/kotlin/org/gdglille/devfest/repositories/AgendaRepository.kt index d54a09109..1bbca9cf7 100644 --- a/shared/core/src/commonMain/kotlin/org/gdglille/devfest/repositories/AgendaRepository.kt +++ b/shared/core/src/commonMain/kotlin/org/gdglille/devfest/repositories/AgendaRepository.kt @@ -123,9 +123,7 @@ class AgendaRepositoryImpl( eventDao.updateTicket(eventId, qrCode, barcode, attendee) } - override fun scaffoldConfig(): Flow = featuresDao.fetchFeatures( - eventId = eventDao.fetchEventId() - ) + override fun scaffoldConfig(): Flow = featuresDao.fetchFeatures() override fun event(): Flow = eventDao.fetchEvent( eventId = eventDao.fetchEventId() diff --git a/shared/models/src/commonMain/kotlin/org/gdglille/devfest/models/inputs/EventInput.kt b/shared/models/src/commonMain/kotlin/org/gdglille/devfest/models/inputs/EventInput.kt index d896cf85f..9acb48a70 100644 --- a/shared/models/src/commonMain/kotlin/org/gdglille/devfest/models/inputs/EventInput.kt +++ b/shared/models/src/commonMain/kotlin/org/gdglille/devfest/models/inputs/EventInput.kt @@ -39,6 +39,16 @@ data class ConferenceHallConfigInput( override fun validate(): List = emptyList() } +@Serializable +data class OpenPlannerConfigInput( + @SerialName("event_id") + val eventId: String, + @SerialName("private_id") + val privateId: String +) : Validator { + override fun validate(): List = emptyList() +} + @Serializable data class BilletWebConfigInput( @SerialName("event_id") @@ -69,6 +79,8 @@ data class EventInput( val openFeedbackId: String?, @SerialName("conference_hall_config") val conferenceHallConfigInput: ConferenceHallConfigInput?, + @SerialName("open_planner_config") + val openPlannerConfigInput: OpenPlannerConfigInput?, @SerialName("billet_web_config") val billetWebConfig: BilletWebConfigInput?, @SerialName("wld_config") diff --git a/theme-m3/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigationViewModel.kt b/theme-m3/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigationViewModel.kt index d9ff083c2..7d77d9074 100644 --- a/theme-m3/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigationViewModel.kt +++ b/theme-m3/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigationViewModel.kt @@ -34,8 +34,8 @@ class MainNavigationViewModel( .map { MainNavigationUiState.Success(navActions(it)) } .catch { MainNavigationUiState.Failure(it) } .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), initialValue = MainNavigationUiState.Loading )