Skip to content

Commit

Permalink
feat(backends): create verbatim sheets.
Browse files Browse the repository at this point in the history
* Create a verbatim for one talk
* Create verbatim for every talks
  • Loading branch information
GerardPaligot committed Mar 29, 2024
1 parent 93ab20a commit fc679ed
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 10 deletions.
3 changes: 3 additions & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dependencies {
implementation(libs.google.cloud.firestore)
implementation(libs.google.cloud.storage)
implementation(libs.google.cloud.secret)
implementation(libs.google.api.client)
implementation(libs.google.auth.client)
implementation(libs.google.drive)
}

appengine {
Expand Down
33 changes: 31 additions & 2 deletions backend/src/main/java/org/gdglille/devfest/backend/Server.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.gdglille.devfest.backend

import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.drive.Drive
import com.google.api.services.drive.DriveScopes
import com.google.auth.http.HttpCredentialsAdapter
import com.google.auth.oauth2.GoogleCredentials
import com.google.cloud.firestore.FirestoreOptions
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient
Expand Down Expand Up @@ -31,6 +36,7 @@ import org.gdglille.devfest.backend.formats.FormatDao
import org.gdglille.devfest.backend.formats.registerFormatsRoutes
import org.gdglille.devfest.backend.internals.helpers.database.BasicDatabase
import org.gdglille.devfest.backend.internals.helpers.database.Database
import org.gdglille.devfest.backend.internals.helpers.drive.GoogleDriveDataSource
import org.gdglille.devfest.backend.internals.helpers.image.TranscoderImage
import org.gdglille.devfest.backend.internals.helpers.secret.Secret
import org.gdglille.devfest.backend.internals.helpers.storage.Storage
Expand Down Expand Up @@ -85,6 +91,15 @@ fun main() {
}.build()
)
)
val driveService = Drive.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
GsonFactory.getDefaultInstance(),
HttpCredentialsAdapter(
GoogleCredentials.getApplicationDefault().createScoped(setOf(DriveScopes.DRIVE))
)
)
.setApplicationName(gcpProjectId)
.build()
val database = Database.Factory.create(firestore = firestore, projectName = projectName)
val basicDatabase = BasicDatabase.Factory.create(firestore = firestore)
val storage = Storage.Factory.create(
Expand All @@ -101,6 +116,7 @@ fun main() {
val partnerDao = PartnerDao(database, storage)
val jobDao = JobDao(database)
val qAndADao = QAndADao(database)
val driveDataSource = GoogleDriveDataSource(driveService)
val wldApi = WeLoveDevsApi.Factory.create(enableNetworkLogs = true)
val geocodeApi = GeocodeApi.Factory.create(
apiKey = secret["GEOCODE_API_KEY"],
Expand Down Expand Up @@ -157,7 +173,14 @@ fun main() {
route("/events/{eventId}") {
registerQAndAsRoutes(eventDao, qAndADao)
registerSpeakersRoutes(eventDao, speakerDao)
registerTalksRoutes(eventDao, speakerDao, talkDao, categoryDao, formatDao)
registerTalksRoutes(
eventDao,
speakerDao,
talkDao,
categoryDao,
formatDao,
driveDataSource
)
registerCategoriesRoutes(eventDao, categoryDao)
registerFormatsRoutes(eventDao, formatDao)
registerSchedulersRoutes(
Expand All @@ -171,7 +194,13 @@ fun main() {
registerPartnersRoutes(geocodeApi, eventDao, partnerDao, jobDao, imageTranscoder)
// Third parties
registerBilletWebRoutes(eventDao)
registerCms4PartnersRoutes(geocodeApi, eventDao, partnerDao, jobDao, imageTranscoder)
registerCms4PartnersRoutes(
geocodeApi,
eventDao,
partnerDao,
jobDao,
imageTranscoder
)
registerConferenceHallRoutes(
conferenceHallApi,
eventDao,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.gdglille.devfest.backend.internals.helpers.drive

import com.google.api.services.drive.Drive

interface DriveDataSource {
fun findDriveByName(name: String): String?
fun findFolderByName(driveId: String, parentId: String?, name: String): String?
fun findFileByName(driveId: String, parentId: String?, name: String): String?
fun copyFile(driveId: String, fileId: String, name: String): String
fun moveFile(driveId: String, fileId: String, folderId: String): List<String>
fun grantPermission(fileId: String, email: String)

object Factory {
fun create(service: Drive): DriveDataSource = GoogleDriveDataSource(service)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.gdglille.devfest.backend.internals.helpers.drive

import com.google.api.services.drive.Drive
import com.google.api.services.drive.model.File
import com.google.api.services.drive.model.Permission

private const val PageSize = 10

class GoogleDriveDataSource(private val service: Drive) : DriveDataSource {
override fun findDriveByName(name: String): String? {
val drives = service.drives().list().apply {
pageSize = PageSize
fields = "nextPageToken, drives(id, name)"
}.execute()
return drives.drives.find { it.name == name }?.id
}

override fun findFolderByName(driveId: String, parentId: String?, name: String): String? {
return service.files().list().apply {
this.driveId = driveId
includeItemsFromAllDrives = true
supportsAllDrives = true
corpora = "drive"
pageSize = PageSize
fields = "nextPageToken, files(id)"
this.q = "name='$name'${parentId?.let { " and parents='$parentId'" } ?: run { "" }}"
}.execute().files.firstOrNull()?.id
}

override fun findFileByName(driveId: String, parentId: String?, name: String): String? {
return service.files().list().apply {
this.driveId = driveId
includeItemsFromAllDrives = true
supportsAllDrives = true
corpora = "drive"
pageSize = PageSize
fields = "nextPageToken, files(id)"
this.q = "name='$name'${parentId?.let { " and parents='$parentId'" } ?: run { "" }}"
}.execute().files.firstOrNull()?.id
}

override fun copyFile(driveId: String, fileId: String, name: String): String {
val file = File().apply {
this.name = name
this.mimeType = "application/vnd.google-apps.spreadsheet"
parents = listOf(driveId)
}
return service.files().copy(fileId, file).apply {
supportsAllDrives = true
fields = "id"
}.execute().id
}

override fun moveFile(driveId: String, fileId: String, folderId: String): List<String> {
val file = service.files()[fileId].apply {
supportsAllDrives = true
fields = "parents"
}.execute()
return service.files().update(fileId, null).apply {
supportsAllDrives = true
addParents = folderId
removeParents = file.parents.joinToString(",")
fields = "parents"
}.execute().parents
}

override fun grantPermission(fileId: String, email: String) {
val userPermission: Permission = Permission()
.setType("user")
.setRole("writer")
userPermission.setEmailAddress(email)
service.permissions().create(fileId, userPermission).setFields("id").execute()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import org.gdglille.devfest.backend.NotFoundException
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.internals.helpers.drive.GoogleDriveDataSource
import org.gdglille.devfest.backend.speakers.SpeakerDao
import org.gdglille.devfest.backend.speakers.convertToModel
import org.gdglille.devfest.models.inputs.TalkInput
import org.gdglille.devfest.models.inputs.TalkVerbatimInput

class TalkRepository(
private val eventDao: EventDao,
private val speakerDao: SpeakerDao,
private val talkDao: TalkDao,
private val categoryDao: CategoryDao,
private val formatDao: FormatDao
private val formatDao: FormatDao,
private val driveDataSource: GoogleDriveDataSource
) {
suspend fun list(eventId: String) = coroutineScope {
val eventDb = eventDao.get(eventId) ?: throw NotFoundException("Event $eventId Not Found")
Expand Down Expand Up @@ -65,4 +67,62 @@ class TalkRepository(
eventDao.updateAgendaUpdatedAt(event)
return@coroutineScope talkId
}

suspend fun verbatim(eventId: String, verbatim: TalkVerbatimInput) = coroutineScope {
val driveId = driveDataSource.findDriveByName(verbatim.driveName)
?: throw NotFoundException("Drive ${verbatim.driveName} doesn't exist")
val eventFolderId = driveDataSource.findFolderByName(driveId, null, verbatim.eventFolder)
?: throw NotFoundException("Folder ${verbatim.eventFolder} doesn't exist")
val targetFolderId = driveDataSource.findFolderByName(driveId, eventFolderId, verbatim.targetFolder)
?: throw NotFoundException("Folder ${verbatim.targetFolder} doesn't exist")
val templateId = driveDataSource.findFileByName(driveId, null, verbatim.templateName)
?: throw NotFoundException("File ${verbatim.templateName} doesn't exist")
val talks = talkDao.getAll(eventId)
val asyncVerbatims = talks.map { talkDb ->
async { verbatimByTalk(eventId, talkDb.id, driveId, targetFolderId, templateId) }
}
return@coroutineScope asyncVerbatims.awaitAll()
}

suspend fun verbatim(
eventId: String,
talkId: String,
verbatim: TalkVerbatimInput
) = coroutineScope {
val driveId = driveDataSource.findDriveByName(verbatim.driveName)
?: throw NotFoundException("Drive ${verbatim.driveName} doesn't exist")
val eventFolderId = driveDataSource.findFolderByName(driveId, null, verbatim.eventFolder)
?: throw NotFoundException("Folder ${verbatim.eventFolder} doesn't exist")
val targetFolderId = driveDataSource.findFolderByName(driveId, eventFolderId, verbatim.targetFolder)
?: throw NotFoundException("Folder ${verbatim.targetFolder} doesn't exist")
val templateId = driveDataSource.findFileByName(driveId, null, verbatim.templateName)
?: throw NotFoundException("File ${verbatim.templateName} doesn't exist")
return@coroutineScope verbatimByTalk(eventId, talkId, driveId, targetFolderId, templateId)
}

private suspend fun verbatimByTalk(
eventId: String,
talkId: String,
driveId: String,
targetFolderId: String,
templateId: String
) = coroutineScope {
val talkDb = talkDao.get(eventId, talkId)
?: throw NotFoundException("Talk $talkId doesn't exist")
val emailSpeakers = speakerDao.getByIds(eventId, talkDb.speakerIds)
.filter { it.email != null }
.map { it.email!! }
if (emailSpeakers.isEmpty()) {
throw NotFoundException("No speakers in talk $talkId")
}
val sheetName = "Verbatims ${talkDb.title}"
val sheetId = driveDataSource.findFileByName(driveId, targetFolderId, sheetName)
if (sheetId != null) {
return@coroutineScope sheetId
}
val fileId = driveDataSource.copyFile(driveId, templateId, sheetName)
driveDataSource.moveFile(driveId, fileId, targetFolderId)
emailSpeakers.forEach { driveDataSource.grantPermission(fileId = fileId, email = it) }
return@coroutineScope fileId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,29 @@ import io.ktor.server.routing.put
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.internals.helpers.drive.GoogleDriveDataSource
import org.gdglille.devfest.backend.receiveValidated
import org.gdglille.devfest.backend.speakers.SpeakerDao
import org.gdglille.devfest.models.inputs.TalkInput
import org.gdglille.devfest.models.inputs.TalkVerbatimInput

@Suppress("LongParameterList")
fun Route.registerTalksRoutes(
eventDao: EventDao,
speakerDao: SpeakerDao,
talkDao: TalkDao,
categoryDao: CategoryDao,
formatDao: FormatDao
formatDao: FormatDao,
driveDataSource: GoogleDriveDataSource
) {
val repository = TalkRepository(eventDao, speakerDao, talkDao, categoryDao, formatDao)
val repository = TalkRepository(
eventDao,
speakerDao,
talkDao,
categoryDao,
formatDao,
driveDataSource
)

get("/talks") {
val eventId = call.parameters["eventId"]!!
Expand All @@ -38,11 +49,26 @@ fun Route.registerTalksRoutes(
val talkInput = call.receiveValidated<TalkInput>()
call.respond(HttpStatusCode.Created, repository.create(eventId, apiKey, talkInput))
}
post("talks/verbatim") {
val eventId = call.parameters["eventId"]!!
val verbatim = call.receiveValidated<TalkVerbatimInput>()
val verbatims = repository.verbatim(eventId, verbatim)
call.respond(
status = if (verbatims.isEmpty()) HttpStatusCode.NoContent else HttpStatusCode.Created,
message = verbatims
)
}
put("/talks/{id}") {
val eventId = call.parameters["eventId"]!!
val apiKey = call.request.headers["api_key"]!!
val talkId = call.parameters["id"]!!
val talkInput = call.receiveValidated<TalkInput>()
call.respond(HttpStatusCode.OK, repository.update(eventId, apiKey, talkId, talkInput))
}
post("talks/{id}/verbatim") {
val eventId = call.parameters["eventId"]!!
val talkId = call.parameters["id"]!!
val verbatim = call.receiveValidated<TalkVerbatimInput>()
call.respond(HttpStatusCode.Created, repository.verbatim(eventId, talkId, verbatim))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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.speakers.convertToDb
import org.gdglille.devfest.backend.talks.TalkDb

fun CategoryOP.convertToDb() = CategoryDb(
Expand Down
14 changes: 11 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ coil = "2.5.0"
detekt = "1.23.4"
font-awesome = "1.1.0"
google-accompanist = "0.32.0"
google-cloud-firestore = "3.7.3"
google-cloud-secretmanager = "2.3.10"
google-cloud-storage = "2.3.0"
google-api-client = "2.3.0"
google-api-services-drive = "v3-rev20220815-2.0.0"
google-api-services-sheets = "v4-rev20220927-2.0.0"
google-cloud-firestore = "3.17.1"
google-cloud-secretmanager = "2.36.0"
google-cloud-storage = "2.34.0"
google-oauth-client = "1.35.0"
google-firebase = "32.2.3"
google-firebase-crashlytics-gradlePlugin = "2.9.9"
google-material = "1.11.0"
Expand Down Expand Up @@ -93,6 +97,10 @@ google-accompanist-permissions = { group = "com.google.accompanist", name = "acc
google-cloud-firestore = { group = "com.google.cloud", name = "google-cloud-firestore", version.ref = "google-cloud-firestore" }
google-cloud-storage = { group = "com.google.cloud", name = "google-cloud-storage", version.ref = "google-cloud-storage" }
google-cloud-secret = { group = "com.google.cloud", name = "google-cloud-secretmanager", version.ref = "google-cloud-secretmanager" }
google-api-client = { group = "com.google.api-client", name = "google-api-client", version.ref = "google-api-client" }
google-auth-client = { group = "com.google.oauth-client", name = "google-oauth-client-jetty", version.ref = "google-oauth-client" }
google-sheets = { group = "com.google.apis", name = "google-api-services-sheets", version.ref = "google-api-services-sheets" }
google-drive = { group = "com.google.apis", name = "google-api-services-drive", version.ref = "google-api-services-drive" }
google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "google-firebase" }
google-firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "google-firebase-crashlytics-gradlePlugin" }
google-material = { group = "com.google.android.material", name = "material", version.ref = "google-material" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.gdglille.devfest.models.inputs

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TalkVerbatimInput(
@SerialName("drive_name")
val driveName: String,
@SerialName("event_folder")
val eventFolder: String,
@SerialName("target_folder")
val targetFolder: String,
@SerialName("template_name")
val templateName: String
) : Validator {
override fun validate(): List<String> = emptyList()
}

0 comments on commit fc679ed

Please sign in to comment.