diff --git a/Sources/src/main/kotlin/allin/Application.kt b/Sources/src/main/kotlin/allin/Application.kt index 90c1e3e..ce9d498 100644 --- a/Sources/src/main/kotlin/allin/Application.kt +++ b/Sources/src/main/kotlin/allin/Application.kt @@ -5,8 +5,8 @@ import allin.data.mock.MockDataSource import allin.data.postgres.PostgresDataSource import allin.routing.* import allin.utils.TokenManager +import allin.utils.kronJob import com.typesafe.config.ConfigFactory -import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* @@ -14,6 +14,13 @@ import io.ktor.server.config.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* +import kotlinx.serialization.json.Json +import java.time.ZonedDateTime +import kotlin.time.ExperimentalTime +import kotlin.time.minutes + +@ExperimentalTime +val BET_VERIFY_DELAY = 5.minutes val data_source = System.getenv()["DATA_SOURCE"] @@ -25,12 +32,21 @@ private val allInDataSource: AllInDataSource = when (data_source) { val Application.dataSource: AllInDataSource get() = allInDataSource + +val json by lazy { + Json { + ignoreUnknownKeys = true + encodeDefaults = true + } +} + fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0") { extracted() }.start(wait = true) } +@OptIn(ExperimentalTime::class) private fun Application.extracted() { val config = HoconApplicationConfig(ConfigFactory.load()) val tokenManager = TokenManager.getInstance(config) @@ -46,11 +62,16 @@ private fun Application.extracted() { } } install(ContentNegotiation) { - json() + json } + BasicRouting() UserRouter() BetRouter() ParticipationRouter() BetDetailRouter() + + kronJob(BET_VERIFY_DELAY) { + dataSource.betDataSource.updateBetStatuses(ZonedDateTime.now()) + } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/AllInDataSource.kt b/Sources/src/main/kotlin/allin/data/AllInDataSource.kt index a0861bb..f2c1406 100644 --- a/Sources/src/main/kotlin/allin/data/AllInDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/AllInDataSource.kt @@ -1,7 +1,12 @@ package allin.data abstract class AllInDataSource { + abstract val userDataSource: UserDataSource + + abstract val betDataSource: BetDataSource + + abstract val participationDataSource: ParticipationDataSource } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/BetDataSource.kt b/Sources/src/main/kotlin/allin/data/BetDataSource.kt index 1a4f510..cb543e0 100644 --- a/Sources/src/main/kotlin/allin/data/BetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/BetDataSource.kt @@ -2,6 +2,7 @@ package allin.data import allin.model.Bet import allin.model.UpdatedBetData +import java.time.ZonedDateTime interface BetDataSource { fun getAllBets(): List @@ -10,4 +11,5 @@ interface BetDataSource { fun addBet(bet: Bet) fun removeBet(id: String): Boolean fun updateBet(data: UpdatedBetData): Boolean + fun updateBetStatuses(date: ZonedDateTime) } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt index 2c6f837..d0dfb09 100644 --- a/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/ParticipationDataSource.kt @@ -4,8 +4,15 @@ import allin.model.Participation interface ParticipationDataSource { + fun addParticipation(participation: Participation) + + fun getParticipationFromBetId(betid: String): List + + fun getParticipationFromUserId(username: String, betid: String): List + + fun deleteParticipation(id: String): Boolean } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/UserDataSource.kt b/Sources/src/main/kotlin/allin/data/UserDataSource.kt index a40776c..98c5eec 100644 --- a/Sources/src/main/kotlin/allin/data/UserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/UserDataSource.kt @@ -4,9 +4,18 @@ import allin.dto.UserDTO import allin.model.User interface UserDataSource { + fun getUserByUsername(username: String): Pair + + fun addUser(user: User) + + fun deleteUser(username: String): Boolean + + fun modifyUserCoins(username: String, amount: Int) + + fun userExists(username: String, email: String): Boolean } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt b/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt index 3dbed63..3d8c780 100644 --- a/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/mock/MockBetDataSource.kt @@ -2,6 +2,7 @@ package allin.data.mock import allin.data.BetDataSource import allin.model.Bet +import allin.model.BetStatus import allin.model.UpdatedBetData import java.time.ZonedDateTime @@ -26,6 +27,18 @@ class MockBetDataSource : BetDataSource { bets += bet } + override fun updateBetStatuses(date: ZonedDateTime) { + bets.forEachIndexed { idx, bet -> + if (bet.endRegistration >= date) { + if (bet.endBet >= date) { + bets[idx] = bet.copy(status = BetStatus.WAITING) + } else { + bets[idx] = bet.copy(status = BetStatus.CLOSING) + } + } + } + } + private val bets by lazy { mutableListOf() } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt index 373694f..ffad229 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresBetDataSource.kt @@ -2,9 +2,14 @@ package allin.data.postgres import allin.data.BetDataSource import allin.entities.BetsEntity +import allin.entities.NO_VALUE +import allin.entities.ResponsesEntity +import allin.entities.ResponsesEntity.response +import allin.entities.YES_VALUE import allin.model.Bet +import allin.model.BetStatus +import allin.model.BetType import allin.model.UpdatedBetData -import allin.utils.Execute import org.ktorm.database.Database import org.ktorm.dsl.* import java.time.ZoneId @@ -21,8 +26,21 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource { endRegistration = this[BetsEntity.endRegistration]!!.atZone(ZoneId.of("Europe/Paris")), endBet = this[BetsEntity.endBet]!!.atZone(ZoneId.of("Europe/Paris")), isPrivate = this[BetsEntity.isPrivate] ?: false, - response = mutableListOf(), // ResponsesEntity.getResponse(UUID.fromString(this[BetsEntity.id].toString())), - createdBy = this[BetsEntity.createdBy].toString() + status = this[BetsEntity.status] ?: BetStatus.IN_PROGRESS, + type = this[BetsEntity.type] ?: BetType.CUSTOM, + createdBy = this[BetsEntity.createdBy].toString(), + response = let { + val idBet = this[BetsEntity.id].toString() + val type = this[BetsEntity.type] ?: BetType.CUSTOM + if (type == BetType.CUSTOM) { + database.from(ResponsesEntity) + .select(response) + .where { ResponsesEntity.id eq UUID.fromString(idBet) } + .map { it[response].toString() } + } else { + listOf(YES_VALUE, NO_VALUE) + } + } ) private fun Query.mapToBet() = this.map { it.toBet() } @@ -53,7 +71,15 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource { set(it.isPrivate, bet.isPrivate) set(it.createdBy, bet.createdBy) } - // ResponsesEntity.addResponse(bet.response, UUID.fromString(bet.id)) + + if (bet.type == BetType.CUSTOM) { + bet.response.forEach { selected -> + database.insert(ResponsesEntity) { + set(it.id, UUID.fromString(bet.id)) + set(it.response, selected) + } + } + } } override fun removeBet(id: String): Boolean { @@ -67,9 +93,21 @@ class PostgresBetDataSource(private val database: Database) : BetDataSource { } > 0 } - fun createBetsTable() { - val request = - "CREATE TABLE IF not exists bet ( id uuid PRIMARY KEY, theme VARCHAR(255), endregistration timestamp,endbet timestamp,sentencebet varchar(500),isprivate boolean, createdby varchar(250))" - database.Execute(request) + override fun updateBetStatuses(date: ZonedDateTime) { + database.update(BetsEntity) { + set(BetsEntity.status, BetStatus.WAITING) + where { + (BetsEntity.endRegistration greaterEq date.toInstant()) and + (BetsEntity.endBet less date.toInstant()) + } + } + + database.update(BetsEntity) { + set(BetsEntity.status, BetStatus.CLOSING) + where { + (BetsEntity.endRegistration greaterEq date.toInstant()) and + (BetsEntity.endBet greaterEq date.toInstant()) + } + } } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt index 28d6d6e..bb9fb19 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresDataSource.kt @@ -4,6 +4,7 @@ import allin.data.AllInDataSource import allin.data.BetDataSource import allin.data.ParticipationDataSource import allin.data.UserDataSource +import allin.utils.Execute import org.ktorm.database.Database class PostgresDataSource : AllInDataSource() { @@ -21,14 +22,14 @@ class PostgresDataSource : AllInDataSource() { user = dbUser, password = dbPassword ) + + database.Execute("CREATE TABLE IF not exists utilisateur ( id uuid PRIMARY KEY, username VARCHAR(255), password VARCHAR(255),coins double precision,email VARCHAR(255))") + database.Execute("CREATE TABLE IF not exists bet ( id uuid PRIMARY KEY, theme VARCHAR(255), endregistration timestamp,endbet timestamp,sentencebet varchar(500),isprivate boolean, createdby varchar(250))") + database.Execute("CREATE TABLE IF NOT EXISTS participation (id uuid PRIMARY KEY,bet uuid,username varchar(250),answer varchar(250),stake int);") + database.Execute("CREATE TABLE IF NOT EXISTS response (id UUID,response VARCHAR(250),CONSTRAINT pk_response_id PRIMARY KEY (id,response));") } override val userDataSource: UserDataSource = PostgresUserDataSource(database) - .also { it.createUserTable() } - override val betDataSource: BetDataSource = PostgresBetDataSource(database) - .also { it.createBetsTable() } - override val participationDataSource: ParticipationDataSource = PostgresParticipationDataSource(database) - .also { it.createParticipationTable() } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt index 5c43f9d..ad50c6e 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresParticipationDataSource.kt @@ -3,7 +3,6 @@ package allin.data.postgres import allin.data.ParticipationDataSource import allin.entities.ParticipationsEntity import allin.model.Participation -import allin.utils.Execute import org.ktorm.database.Database import org.ktorm.dsl.* import java.util.* @@ -21,12 +20,6 @@ class PostgresParticipationDataSource(private val database: Database) : Particip private fun Query.mapToParticipation() = this.map { it.toParticipation() } - fun createParticipationTable() { - val request = - "CREATE TABLE IF NOT EXISTS participation (id uuid PRIMARY KEY,bet uuid,username varchar(250),answer varchar(250),stake int);" - database.Execute(request) - } - override fun addParticipation(participation: Participation) { database.insert(ParticipationsEntity) { set(it.id, UUID.fromString(participation.id)) diff --git a/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt b/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt index 16a7f37..61723ac 100644 --- a/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt +++ b/Sources/src/main/kotlin/allin/data/postgres/PostgresUserDataSource.kt @@ -55,10 +55,4 @@ class PostgresUserDataSource(private val database: Database) : UserDataSource { val request = "UPDATE utilisateur SET coins = coins - $amount WHERE username = '$username';" database.Execute(request) } - - fun createUserTable() { - val request = - "CREATE TABLE IF not exists utilisateur ( id uuid PRIMARY KEY, username VARCHAR(255), password VARCHAR(255),coins double precision,email VARCHAR(255))" - database.Execute(request) - } } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/entities/BetEntity.kt b/Sources/src/main/kotlin/allin/entities/BetEntity.kt index b4650cf..1e8009d 100644 --- a/Sources/src/main/kotlin/allin/entities/BetEntity.kt +++ b/Sources/src/main/kotlin/allin/entities/BetEntity.kt @@ -1,5 +1,7 @@ package allin.entities +import allin.model.BetStatus +import allin.model.BetType import org.ktorm.entity.Entity import org.ktorm.schema.* import java.time.ZonedDateTime @@ -11,6 +13,8 @@ interface BetEntity : Entity { val endRegistration: ZonedDateTime val endBet: ZonedDateTime val isPrivate: Boolean + val status: BetStatus + val type: BetType val createdBy: String } @@ -21,5 +25,7 @@ object BetsEntity : Table("bet") { val endRegistration = timestamp("endregistration") val endBet = timestamp("endbet") val isPrivate = boolean("isprivate") + val status = enum("status") + val type = enum("type") val createdBy = varchar("createdby") } \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/entities/ResponseEntity.kt b/Sources/src/main/kotlin/allin/entities/ResponseEntity.kt index 7df360d..5da1b16 100644 --- a/Sources/src/main/kotlin/allin/entities/ResponseEntity.kt +++ b/Sources/src/main/kotlin/allin/entities/ResponseEntity.kt @@ -7,6 +7,9 @@ import org.ktorm.schema.varchar import java.util.* +const val YES_VALUE = "Yes" +const val NO_VALUE = "No" + interface ResponseEntity : Entity { val betId: UUID val response: String @@ -15,27 +18,4 @@ interface ResponseEntity : Entity { object ResponsesEntity : Table("response") { val id = uuid("id").primaryKey() val response = varchar("response").primaryKey() - - /* - fun createResponseTable(){ - val request="CREATE TABLE IF NOT EXISTS response (id UUID,response VARCHAR(250),CONSTRAINT pk_response_id PRIMARY KEY (id,response));" - database.Execute(request) - } - - fun getResponse(idBet: UUID): MutableList { - return database.from(ResponsesEntity) - .select(response) - .where { id eq idBet } - .map { it[response].toString() }.toMutableList() - } - - fun addResponse(responses : MutableList, idBet : UUID ) { - responses.forEach {selected -> - database.insert(ResponsesEntity) { - set(it.id, idBet) - set(it.response,selected) - } - } - } - */ } diff --git a/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt b/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt index c3091ae..0d5db8c 100644 --- a/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt +++ b/Sources/src/main/kotlin/allin/ext/PipelineContextExt.kt @@ -10,9 +10,11 @@ import io.ktor.server.auth.jwt.* import io.ktor.server.response.* import io.ktor.util.pipeline.* + suspend fun PipelineContext<*, ApplicationCall>.hasToken(content: suspend (principal: JWTPrincipal) -> Unit) = call.principal()?.let { content(it) } ?: call.respond(HttpStatusCode.Unauthorized) + suspend fun PipelineContext<*, ApplicationCall>.verifyUserFromToken( userDataSource: UserDataSource, principal: JWTPrincipal, diff --git a/Sources/src/main/kotlin/allin/model/Bet.kt b/Sources/src/main/kotlin/allin/model/Bet.kt index e088e2b..c27a2f5 100644 --- a/Sources/src/main/kotlin/allin/model/Bet.kt +++ b/Sources/src/main/kotlin/allin/model/Bet.kt @@ -1,20 +1,21 @@ package allin.model -import allin.serializer.UUIDSerializer +import allin.model.BetStatus.IN_PROGRESS import allin.serializer.ZonedDateTimeSerializer import kotlinx.serialization.Serializable import java.time.ZonedDateTime -import java.util.* @Serializable data class Bet( val id: String = "", val theme: String, val sentenceBet: String, + val status: BetStatus = IN_PROGRESS, + val type: BetType, @Serializable(ZonedDateTimeSerializer::class) val endRegistration: ZonedDateTime, @Serializable(ZonedDateTimeSerializer::class) var endBet: ZonedDateTime, var isPrivate: Boolean, - var response: MutableList, + var response: List, val createdBy: String = "" ) @@ -23,5 +24,5 @@ data class UpdatedBetData( val id: String, @Serializable(ZonedDateTimeSerializer::class) val endBet: ZonedDateTime, val isPrivate: Boolean, - val response: MutableList + val response: List ) \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/BetStatus.kt b/Sources/src/main/kotlin/allin/model/BetStatus.kt new file mode 100644 index 0000000..ce29e1b --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/BetStatus.kt @@ -0,0 +1,9 @@ +package allin.model + +enum class BetStatus { + IN_PROGRESS, + WAITING, + CLOSING, + FINISHED, + CANCELLED +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/model/BetType.kt b/Sources/src/main/kotlin/allin/model/BetType.kt new file mode 100644 index 0000000..a6a39c3 --- /dev/null +++ b/Sources/src/main/kotlin/allin/model/BetType.kt @@ -0,0 +1,7 @@ +package allin.model + +enum class BetType { + MATCH, + BINARY, + CUSTOM +} \ No newline at end of file diff --git a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt index 0d78278..7bebf88 100644 --- a/Sources/src/main/kotlin/allin/routing/BasicRouting.kt +++ b/Sources/src/main/kotlin/allin/routing/BasicRouting.kt @@ -4,7 +4,8 @@ import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Application.BasicRouting(){ + +fun Application.BasicRouting() { routing { get("/") { call.respond("Bienvenue sur l'API de AlLin!") diff --git a/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt b/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt index 68de179..a8d9243 100644 --- a/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetDetailRouter.kt @@ -11,6 +11,7 @@ import io.ktor.server.auth.* import io.ktor.server.response.* import io.ktor.server.routing.* + fun Application.BetDetailRouter() { val userDataSource = this.dataSource.userDataSource val betDataSource = this.dataSource.betDataSource diff --git a/Sources/src/main/kotlin/allin/routing/BetRouter.kt b/Sources/src/main/kotlin/allin/routing/BetRouter.kt index fb59b86..5653e3f 100644 --- a/Sources/src/main/kotlin/allin/routing/BetRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/BetRouter.kt @@ -17,6 +17,7 @@ import java.util.* val tokenManagerBet = AppConfig.tokenManager + fun Application.BetRouter() { val userDataSource = this.dataSource.userDataSource val betDataSource = this.dataSource.betDataSource @@ -34,14 +35,16 @@ fun Application.BetRouter() { call.respond(HttpStatusCode.Conflict, ApiMessage.BetAlreadyExist) } ?: run { val betWithId = Bet( - id, - bet.theme, - bet.sentenceBet, - bet.endRegistration, - bet.endBet, - bet.isPrivate, - bet.response, - username + id = id, + theme = bet.theme, + sentenceBet = bet.sentenceBet, + status = bet.status, + type = bet.type, + endRegistration = bet.endRegistration, + endBet = bet.endBet, + isPrivate = bet.isPrivate, + response = bet.response, + createdBy = username ) betDataSource.addBet(betWithId) call.respond(HttpStatusCode.Created, betWithId) diff --git a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt index 58b09c8..bab948b 100644 --- a/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/ParticipationRouter.kt @@ -14,6 +14,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import java.util.* + fun Application.ParticipationRouter() { val userDataSource = this.dataSource.userDataSource diff --git a/Sources/src/main/kotlin/allin/routing/UserRouter.kt b/Sources/src/main/kotlin/allin/routing/UserRouter.kt index fb39d0c..2315d76 100644 --- a/Sources/src/main/kotlin/allin/routing/UserRouter.kt +++ b/Sources/src/main/kotlin/allin/routing/UserRouter.kt @@ -20,6 +20,8 @@ val RegexCheckerUser = AppConfig.regexChecker val CryptManagerUser = AppConfig.cryptManager val tokenManagerUser = AppConfig.tokenManager const val DEFAULT_COINS = 500 + + fun Application.UserRouter() { val userDataSource = this.dataSource.userDataSource diff --git a/Sources/src/main/kotlin/allin/utils/kronJob.kt b/Sources/src/main/kotlin/allin/utils/kronJob.kt new file mode 100644 index 0000000..f27710d --- /dev/null +++ b/Sources/src/main/kotlin/allin/utils/kronJob.kt @@ -0,0 +1,16 @@ +package allin.utils + +import kotlinx.coroutines.* +import kotlin.time.Duration +import kotlin.time.ExperimentalTime + +@OptIn(DelicateCoroutinesApi::class, ExperimentalTime::class) +fun kronJob(duration: Duration, action: () -> Unit) = + GlobalScope.launch { + withContext(Dispatchers.IO) { + while (true) { + runCatching { action() } + delay(duration.toLongMilliseconds()) + } + } + } \ No newline at end of file