diff --git a/shared/src/commonMain/kotlin/app/opass/ccip/database/OPassDatabaseHelper.kt b/shared/src/commonMain/kotlin/app/opass/ccip/database/OPassDatabaseHelper.kt index f37498b..20bbbd7 100644 --- a/shared/src/commonMain/kotlin/app/opass/ccip/database/OPassDatabaseHelper.kt +++ b/shared/src/commonMain/kotlin/app/opass/ccip/database/OPassDatabaseHelper.kt @@ -5,6 +5,7 @@ package app.opass.ccip.database +import app.opass.ccip.extensions.toAttendee import app.opass.ccip.extensions.toEvent import app.opass.ccip.extensions.toEventConfig import app.opass.ccip.extensions.toLocalizedObject @@ -13,6 +14,7 @@ import app.opass.ccip.extensions.toSpeaker import app.opass.ccip.network.models.common.LocalizedObject import app.opass.ccip.network.models.event.Event import app.opass.ccip.network.models.eventconfig.EventConfig +import app.opass.ccip.network.models.fastpass.Attendee import app.opass.ccip.network.models.schedule.Schedule import app.opass.ccip.network.models.schedule.Session import app.opass.ccip.network.models.schedule.Speaker @@ -253,4 +255,33 @@ internal class OPassDatabaseHelper { sessions = getSessions(eventId) ) } + + suspend fun getAttendee(eventId: String, token: String): Attendee? { + return withContext(Dispatchers.IO) { + dbQuery.selectAttendee(eventId, token).executeAsOneOrNull()?.toAttendee() + } + } + + suspend fun deleteAttendee(eventId: String, token: String) { + return withContext(Dispatchers.IO) { + dbQuery.deleteAttendee(eventId, token) + } + } + + suspend fun addAttendee(eventId: String, attendee: Attendee) { + withContext(Dispatchers.IO) { + dbQuery.transaction { + dbQuery.deleteAttendee(eventId, attendee.token) + dbQuery.insertAttendee( + userId = attendee.userId, + attr = Json.encodeToString(attendee.attr), + firstUse = attendee.firstUse, + role = attendee.role, + scenarios = Json.encodeToString(attendee.scenarios), + token = attendee.token, + eventId = eventId + ) + } + } + } } diff --git a/shared/src/commonMain/kotlin/app/opass/ccip/extensions/AttendeeTable.kt b/shared/src/commonMain/kotlin/app/opass/ccip/extensions/AttendeeTable.kt new file mode 100644 index 0000000..4c4a6cf --- /dev/null +++ b/shared/src/commonMain/kotlin/app/opass/ccip/extensions/AttendeeTable.kt @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 OPass + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.opass.ccip.extensions + +import app.opass.ccip.database.AttendeeTable +import app.opass.ccip.network.models.fastpass.Attendee +import app.opass.ccip.network.models.fastpass.Scenario +import kotlinx.serialization.json.Json + +internal fun AttendeeTable.toAttendee(): Attendee { + return Attendee( + attr = Json.decodeFromString>(this.attr), + eventId = this.eventId, + firstUse = this.firstUse, + role = this.role, + scenarios = Json.decodeFromString>(this.scenarios), + token = this.token, + userId = this.eventId, + ) +} diff --git a/shared/src/commonMain/kotlin/app/opass/ccip/helpers/PortalHelper.kt b/shared/src/commonMain/kotlin/app/opass/ccip/helpers/PortalHelper.kt index 76ba182..435a956 100644 --- a/shared/src/commonMain/kotlin/app/opass/ccip/helpers/PortalHelper.kt +++ b/shared/src/commonMain/kotlin/app/opass/ccip/helpers/PortalHelper.kt @@ -11,6 +11,7 @@ import app.opass.ccip.network.models.common.LocalizedObject import app.opass.ccip.network.models.event.Event import app.opass.ccip.network.models.eventconfig.EventConfig import app.opass.ccip.network.models.eventconfig.FeatureType +import app.opass.ccip.network.models.fastpass.Attendee import app.opass.ccip.network.models.schedule.Schedule import app.opass.ccip.network.models.schedule.Session import app.opass.ccip.network.models.schedule.Speaker @@ -78,6 +79,41 @@ class PortalHelper { } } + /** + * Fetches [Attendee] for specified event using given token from event's FastPass feature + * @param eventId ID of the event + * @param token Token to identify attendee + * @param forceReload Whether to ignore cache, false by default + * @return null if attendee hasn't been cached yet or token is invalid; Attendee otherwise + */ + suspend fun getAttendee( + eventId: String, + token: String, + forceReload: Boolean = false + ): Attendee? { + val eventConfig = dbHelper.getEventConfig(eventId) ?: return null + val feat = eventConfig.features.find { f -> f.type == FeatureType.FAST_PASS } ?: return null + + val cachedAttendee = dbHelper.getAttendee(eventId, token) + return if (cachedAttendee != null && !forceReload) { + cachedAttendee + } else { + client.getFastPassStatus(feat.url!!, token).also { + dbHelper.addAttendee(eventId, it) + } + } + } + + /** + * Deletes an attendee's information from the database. Apps may call this method when a user + * wants to logout from the OPass app. + * @param eventId ID of the event + * @param token Token to identify attendee + */ + suspend fun deleteAttendee(eventId: String, token: String) { + dbHelper.deleteAttendee(eventId, token) + } + /** * Fetches [Schedule] for specified id from Event's website * @param eventId ID of the event diff --git a/shared/src/commonMain/kotlin/app/opass/ccip/network/PortalClient.kt b/shared/src/commonMain/kotlin/app/opass/ccip/network/PortalClient.kt index f3b692b..5e80957 100644 --- a/shared/src/commonMain/kotlin/app/opass/ccip/network/PortalClient.kt +++ b/shared/src/commonMain/kotlin/app/opass/ccip/network/PortalClient.kt @@ -7,12 +7,15 @@ package app.opass.ccip.network import app.opass.ccip.network.models.event.Event import app.opass.ccip.network.models.eventconfig.EventConfig +import app.opass.ccip.network.models.fastpass.Attendee import app.opass.ccip.network.models.schedule.Schedule import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.url import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json @@ -44,4 +47,11 @@ internal class PortalClient { suspend fun getEventSchedule(url: String): Schedule { return universalClient.get(url).body() } + + suspend fun getFastPassStatus(url: String, token: String): Attendee { + return universalClient.get { + url(url) + parameter("token", token) + }.body() + } } diff --git a/shared/src/commonMain/kotlin/app/opass/ccip/network/models/fastpass/Attendee.kt b/shared/src/commonMain/kotlin/app/opass/ccip/network/models/fastpass/Attendee.kt new file mode 100644 index 0000000..ff4d224 --- /dev/null +++ b/shared/src/commonMain/kotlin/app/opass/ccip/network/models/fastpass/Attendee.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2024 OPass + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.opass.ccip.network.models.fastpass + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Attendee( + val attr: Map = emptyMap(), + + @SerialName("event_id") + val eventId: String, + + @SerialName("first_use") + val firstUse: Long, + + val role: String, + val scenarios: List, + val token: String, + + @SerialName("user_id") + val userId: String +) diff --git a/shared/src/commonMain/kotlin/app/opass/ccip/network/models/fastpass/Scenario.kt b/shared/src/commonMain/kotlin/app/opass/ccip/network/models/fastpass/Scenario.kt new file mode 100644 index 0000000..6b08306 --- /dev/null +++ b/shared/src/commonMain/kotlin/app/opass/ccip/network/models/fastpass/Scenario.kt @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2024 OPass + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.opass.ccip.network.models.fastpass + +import app.opass.ccip.extensions.localized +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Scenario( + val attr: Map = emptyMap(), + + @SerialName("available_time") + val availableTime: Long, + + val countdown: Int, + + @SerialName("display_text") + val _displayText: Localized, + + @SerialName("expire_time") + val expireTime: Long, + + val disabled: String, + + val id: String, + val order: Int, + val used: Int +) { + val displayText: String + get() = localized(_displayText.en, _displayText.zh) + + @Serializable + data class Localized( + @SerialName("en-US") + val en: String, + + @SerialName("zh-TW") + val zh: String + ) +} diff --git a/shared/src/commonMain/sqldelight/app/opass/ccip/database/OPassDatabase.sq b/shared/src/commonMain/sqldelight/app/opass/ccip/database/OPassDatabase.sq index e37c37f..34083c1 100644 --- a/shared/src/commonMain/sqldelight/app/opass/ccip/database/OPassDatabase.sq +++ b/shared/src/commonMain/sqldelight/app/opass/ccip/database/OPassDatabase.sq @@ -109,6 +109,20 @@ CREATE TABLE SessionTable ( CREATE INDEX idx_SessionTable_title ON SessionTable(titleEn, titleZh, eventId); +-- Create Attendee table and index + +CREATE TABLE AttendeeTable( + primaryId INTEGER PRIMARY KEY AUTOINCREMENT, + userId TEXT NOT NULL, + attr TEXT NOT NULL, + firstUse INTEGER NOT NULL, + role TEXT NOT NULL, + scenarios TEXT NOT NULL, + token TEXT NOT NULL, + eventId TEXT NOT NULL, + FOREIGN KEY (eventId) REFERENCES EventConfigTable(id) ON DELETE CASCADE +); + -- Named queries for event table selectAllEvents: @@ -202,3 +216,15 @@ DELETE FROM SessionTable WHERE eventId = :eventId; insertSession: INSERT INTO SessionTable (id, titleEn, titleZh, descriptionEn, descriptionZh, start, end, room, speakers, tags, type, broadcast, liveUrl, url, coWriteUrl, slide, language, qa, record, eventId) VALUES (:id, :titleEn, :titleZh, :descriptionEn, :descriptionZh, :start, :end, :room, :speakers, :tags, :type, :broadcast, :liveUrl, :url, :coWriteUrl, :slide, :language, :qa, :record, :eventId); + +-- Named queries for attendee table + +selectAttendee: +SELECT * FROM AttendeeTable WHERE eventId = :eventId AND token = :token; + +deleteAttendee: +DELETE FROM AttendeeTable WHERE eventId = :eventId AND token = :token; + +insertAttendee: +INSERT INTO AttendeeTable (userId, attr, firstUse, role, scenarios, token, eventId) +VALUES (:userId, :attr, :firstUse, :role, :scenarios, :token, :eventId);