From a7a5181f1860ce752d25189c232c53c3e3678b97 Mon Sep 17 00:00:00 2001 From: WillDotWhite Date: Tue, 16 Jan 2024 18:14:52 +0000 Subject: [PATCH] Update dep versions and perform some trivial tidy up --- api/build.gradle.kts | 52 ++--- api/gradle.properties | 8 +- .../com/gmtkgamejam/routing/PostRoutes.kt | 201 +++++++++--------- .../com/gmtkgamejam/routing/UserInfoRoutes.kt | 1 - 4 files changed, 132 insertions(+), 130 deletions(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 11c4f01c..51c83325 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,12 +1,12 @@ -val ktor_version: String by project -val koin_version: String by project -val kotlin_version: String by project -val logback_version: String by project +val ktorVersion: String by project +val koinVersion: String by project +val kotlinVersion: String by project +val logbackVersion: String by project plugins { application - kotlin("jvm") version "1.7.10" - kotlin("plugin.serialization") version "1.7.10" + kotlin("jvm") version "1.9.22" + kotlin("plugin.serialization") version "1.9.22" } group = "com.gmtkgamejam" @@ -21,43 +21,43 @@ repositories { } dependencies { - implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("ch.qos.logback:logback-classic:$logbackVersion") // Logging support for Javacord - implementation("org.apache.logging.log4j:log4j-to-slf4j:2.17.2") + implementation("org.apache.logging.log4j:log4j-to-slf4j:2.22.1") // Koin core features - implementation("io.insert-koin:koin-ktor:$koin_version") + implementation("io.insert-koin:koin-ktor:$koinVersion") // DB - implementation("org.litote.kmongo:kmongo:4.8.0") + implementation("org.litote.kmongo:kmongo:4.11.0") // Discord bot implementation("org.javacord:javacord:3.8.0") // Ktor server core, auth, etc - implementation("io.ktor:ktor-server-core-jvm:$ktor_version") - implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") - implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") - implementation("io.ktor:ktor-server-auth-jwt-jvm:$ktor_version") - implementation("io.ktor:ktor-server-cors:$ktor_version") + implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-netty-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-auth-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-auth-jwt-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-cors:$ktorVersion") // Ktor core for making web requests - implementation("io.ktor:ktor-client-core-jvm:$ktor_version") - implementation("io.ktor:ktor-client-cio-jvm:$ktor_version") + implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") + implementation("io.ktor:ktor-client-cio-jvm:$ktorVersion") // Ktor serialisation - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") // HTTP serialisation for HttpClient making external requests - implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") - implementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation-jvm:$ktorVersion") // HTTP serialisation for receiving requests from the front end - implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") - implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion") testImplementation("io.insert-koin:koin-test:3.3.3") - testImplementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.4.1") - testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - testImplementation("io.ktor:ktor-server-test-host:$ktor_version") - testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") + testImplementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.12.1") + testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") + testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") + testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion") } diff --git a/api/gradle.properties b/api/gradle.properties index c55f3e5d..816a9c3c 100644 --- a/api/gradle.properties +++ b/api/gradle.properties @@ -1,12 +1,12 @@ # Application language -kotlin_version=1.7.10 +kotlinVersion=1.9.22 kotlin.code.style=official # Web framework -ktor_version=2.2.3 +ktorVersion=2.3.7 # DI framework for Ktor -koin_version=3.2.0 +koinVersion=3.5.3 # Logging framework -logback_version=1.2.11 +logbackVersion=1.4.14 diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt b/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt index f811e9d6..8c3dde42 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt +++ b/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt @@ -25,6 +25,7 @@ import java.time.LocalDateTime import org.bson.conversions.Bson import org.litote.kmongo.* import kotlin.math.min +import kotlin.reflect.KClass import kotlin.reflect.full.memberProperties import kotlin.text.Regex.Companion.escape @@ -34,85 +35,6 @@ fun Application.configurePostRouting() { val service = PostService() val favouritesService = FavouritesService() - fun getFilterFromParameters(params: Parameters): List { - val filters = mutableListOf(PostItem::deletedAt eq null) - - params["description"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&description=` - ?.map { it -> it.trim() } - // The regex is the easiest way to check if a description contains a given substring - ?.forEach { filters.add(PostItem::description regex escape(it).toRegex(RegexOption.IGNORE_CASE)) } - - val skillsPossessedSearchMode = params["skillsPossessedSearchMode"] ?: "and" - params["skillsPossessed"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&skillsPossessed=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::skillsPossessed contains it } - ?.let { if (skillsPossessedSearchMode == "and") and(it) else or(it) } - ?.let(filters::add) - - val skillsSoughtSearchMode = params["skillsSoughtSearchMode"] ?: "and" - params["skillsSought"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&skillsSought=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::skillsSought contains it } - ?.let { if (skillsSoughtSearchMode == "and") and(it) else or(it) } - ?.let(filters::add) - - params["tools"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&skillsSought=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::preferredTools contains it } - ?.let(filters::addAll) - - params["languages"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&languages=` - ?.map { PostItem::languages contains it } - ?.let { filters.add(or(it)) } - - params["availability"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&availability=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::availability eq it } - // Availabilities are mutually exclusive, so treat it as inclusion search - ?.let { filters.add(or(it)) } - - // If no timezones sent, lack of filters will search all timezones - val timezoneRange = params["timezones"]?.split('/') - if (timezoneRange != null && timezoneRange.size == 2) { - val timezoneStart: Int = timezoneRange[0].toInt() - val timezoneEnd: Int = timezoneRange[1].toInt() - - val timezones: MutableList = mutableListOf() - if (timezoneStart < timezoneEnd) { - // UTC-2 -> UTC+2 should be: [-2, -1, 0, 1, 2] - timezones.addAll((timezoneStart..timezoneEnd)) - } else { - // UTC+9 -> UTC-9 should be: [9, 10, 11, 12, -12, -11, -10, -9] - timezones.addAll((timezoneStart..12)) - timezones.addAll((-12..timezoneEnd)) - } - - // Add all timezone searches as eq checks - // It's brute force, but easier to confirm - timezones - .map { PostItem::timezoneOffsets contains it } - .let { filters.add(or(it)) } - } - - return filters - } - - fun getSortFromParameters(params: Parameters): Bson { - val sortByFieldName = params["sortBy"] ?: "createdAt" - val sortByField = PostItem::class.memberProperties.first { prop -> prop.name == sortByFieldName } - return when (params["sortDir"].toString()) { - "asc" -> ascending(sortByField) - "desc" -> descending(sortByField) - else -> descending(sortByField) - } - } - routing { route("/posts") { get { @@ -158,14 +80,17 @@ fun Application.configurePostRouting() { authService.getTokenSet(call) ?.let { - data.authorId = it.discordId // TODO: What about author name? - data.timezoneOffsets = data.timezoneOffsets.filter { tz -> tz >= -12 && tz <= 12 }.toSet() if (service.getPostByAuthorId(it.discordId) != null) { return@post call.respondJSON( "Cannot have duplicate posts", status = HttpStatusCode.BadRequest ) } + it + } + ?.let { + data.authorId = it.discordId // TODO: What about author name? + data.timezoneOffsets = data.timezoneOffsets.filter { tz -> tz >= -12 && tz <= 12 }.toSet() } ?.let { PostItem.fromCreateDto(data) } ?.let { service.createPost(it) } @@ -216,24 +141,23 @@ fun Application.configurePostRouting() { authService.getTokenSet(call) ?.let { service.getPostByAuthorId(it.discordId) } - ?.let { - // FIXME: Don't just brute force update all given fields - it.author = data.author - ?: it.author // We don't expect user to change, but track username updates - it.description = data.description ?: it.description - it.size = min(data.size ?: it.size, 20) // Limit team sizes to 20 people - it.skillsPossessed = data.skillsPossessed ?: it.skillsPossessed - it.skillsSought = data.skillsSought ?: it.skillsSought - it.preferredTools = data.preferredTools ?: it.preferredTools - it.languages = data.languages ?: it.languages - it.availability = data.availability ?: it.availability - it.updatedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - it.timezoneOffsets = - (data.timezoneOffsets ?: it.timezoneOffsets).filter { tz -> tz >= -12 && tz <= 12 } - .toSet() - - service.updatePost(it) - return@put call.respond(it) + ?.let { post -> + // Ugly-but-functional way to update all of the fields in the DTO + data.author?.also { post.author = it } + data.description?.also { post.description = it } + data.size?.also { post.size = min(it, 20) } + data.skillsPossessed?.also { post.skillsPossessed = it } + data.skillsSought?.also { post.skillsSought = it } + data.preferredTools?.also { post.preferredTools = it } + data.languages?.also { post.languages = it } + data.languages?.also { post.languages = it } + data.availability?.also { post.availability = it } + data.timezoneOffsets?.also { post.timezoneOffsets = it.filter { tz -> tz >= -12 && tz <= 12 }.toSet() } + + post.updatedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + + service.updatePost(post) + return@put call.respond(post) } // TODO: Replace BadRequest with contextual response @@ -286,3 +210,82 @@ fun Application.configurePostRouting() { } } } + +fun getFilterFromParameters(params: Parameters): List { + val filters = mutableListOf(PostItem::deletedAt eq null) + + params["description"]?.split(',') + ?.filter(String::isNotBlank) // Filter out empty `&description=` + ?.map { it -> it.trim() } + // The regex is the easiest way to check if a description contains a given substring + ?.forEach { filters.add(PostItem::description regex escape(it).toRegex(RegexOption.IGNORE_CASE)) } + + val skillsPossessedSearchMode = params["skillsPossessedSearchMode"] ?: "and" + params["skillsPossessed"]?.split(',') + ?.filter(String::isNotBlank) // Filter out empty `&skillsPossessed=` + ?.mapNotNull { enumFromStringSafe(it) } + ?.map { PostItem::skillsPossessed contains it } + ?.let { if (skillsPossessedSearchMode == "and") and(it) else or(it) } + ?.let(filters::add) + + val skillsSoughtSearchMode = params["skillsSoughtSearchMode"] ?: "and" + params["skillsSought"]?.split(',') + ?.filter(String::isNotBlank) // Filter out empty `&skillsSought=` + ?.mapNotNull { enumFromStringSafe(it) } + ?.map { PostItem::skillsSought contains it } + ?.let { if (skillsSoughtSearchMode == "and") and(it) else or(it) } + ?.let(filters::add) + + params["tools"]?.split(',') + ?.filter(String::isNotBlank) // Filter out empty `&skillsSought=` + ?.mapNotNull { enumFromStringSafe(it) } + ?.map { PostItem::preferredTools contains it } + ?.let(filters::addAll) + + params["languages"]?.split(',') + ?.filter(String::isNotBlank) // Filter out empty `&languages=` + ?.map { PostItem::languages contains it } + ?.let { filters.add(or(it)) } + + params["availability"]?.split(',') + ?.filter(String::isNotBlank) // Filter out empty `&availability=` + ?.mapNotNull { enumFromStringSafe(it) } + ?.map { PostItem::availability eq it } + // Availabilities are mutually exclusive, so treat it as inclusion search + ?.let { filters.add(or(it)) } + + // If no timezones sent, lack of filters will search all timezones + val timezoneRange = params["timezones"]?.split('/') + if (timezoneRange != null && timezoneRange.size == 2) { + val timezoneStart: Int = timezoneRange[0].toInt() + val timezoneEnd: Int = timezoneRange[1].toInt() + + val timezones: MutableList = mutableListOf() + if (timezoneStart < timezoneEnd) { + // UTC-2 -> UTC+2 should be: [-2, -1, 0, 1, 2] + timezones.addAll((timezoneStart..timezoneEnd)) + } else { + // UTC+9 -> UTC-9 should be: [9, 10, 11, 12, -12, -11, -10, -9] + timezones.addAll((timezoneStart..12)) + timezones.addAll((-12..timezoneEnd)) + } + + // Add all timezone searches as eq checks + // It's brute force, but easier to confirm + timezones + .map { PostItem::timezoneOffsets contains it } + .let { filters.add(or(it)) } + } + + return filters +} + +fun getSortFromParameters(params: Parameters): Bson { + val sortByFieldName = params["sortBy"] ?: "createdAt" + val sortByField = PostItem::class.memberProperties.first { prop -> prop.name == sortByFieldName } + return when (params["sortDir"].toString()) { + "asc" -> ascending(sortByField) + "desc" -> descending(sortByField) + else -> descending(sortByField) + } +} diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt b/api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt index 302ca556..7c921402 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt +++ b/api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt @@ -79,7 +79,6 @@ fun Application.configureUserInfoRouting() { accessToken = refreshedTokenSet.access_token } - // TODO: Risk of rate limiting from Discord val user = getUserInfoAsync(accessToken) val displayName = bot.getDisplayNameForUser(user.id)