Skip to content

Commit

Permalink
Update dep versions and perform some trivial tidy up
Browse files Browse the repository at this point in the history
  • Loading branch information
Willdotwhite committed Jan 16, 2024
1 parent b826a3a commit a7a5181
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 130 deletions.
52 changes: 26 additions & 26 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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")
}
8 changes: 4 additions & 4 deletions api/gradle.properties
Original file line number Diff line number Diff line change
@@ -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
201 changes: 102 additions & 99 deletions api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -34,85 +35,6 @@ fun Application.configurePostRouting() {
val service = PostService()
val favouritesService = FavouritesService()

fun getFilterFromParameters(params: Parameters): List<Bson> {
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<Skills>(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<Skills>(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<Tools>(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<Availability>(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<Int> = 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 {
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -286,3 +210,82 @@ fun Application.configurePostRouting() {
}
}
}

fun getFilterFromParameters(params: Parameters): List<Bson> {
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<Skills>(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<Skills>(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<Tools>(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<Availability>(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<Int> = 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit a7a5181

Please sign in to comment.