Skip to content

Commit

Permalink
feat(openplanner): add compatibility with OpenPlanner.fr.
Browse files Browse the repository at this point in the history
Closes #60
  • Loading branch information
GerardPaligot committed Nov 20, 2023
1 parent ac1ab62 commit 472258a
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions backend/src/main/java/org/gdglille/devfest/backend/Server.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -128,6 +140,7 @@ fun main() {
routing {
registerEventRoutes(
geocodeApi,
openPlannerApi,
eventDao,
speakerDao,
qAndADao,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "",
Expand All @@ -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 = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -52,6 +55,7 @@ fun Route.registerEventRoutes(
) {
val repository = EventRepository(
geocodeApi,
openPlannerApi,
eventDao,
speakerDao,
qAndADao,
Expand Down Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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<OpenPlanner>()

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
}
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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<SpeakerOP>,
val sessions: List<SessionOP>
)

@Serializable
data class EventOP(
val id: String,
val name: String,
val scheduleVisible: Boolean,
val dateStart: String,
val dateEnd: String,
val formats: List<FormatOP>,
val categories: List<CategoryOP>,
val tracks: List<TrackOP>
)

@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<SocialOP>
)

@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<String>,
val trackId: String?,
val language: String,
val level: String,
val formatId: String,
val categoryId: String,
)
Loading

0 comments on commit 472258a

Please sign in to comment.