Skip to content

Commit

Permalink
Backend: Start/stop logic, events and initial API surface
Browse files Browse the repository at this point in the history
  • Loading branch information
gdude2002 committed Jun 15, 2024
1 parent 2dcb171 commit a5a8feb
Show file tree
Hide file tree
Showing 12 changed files with 482 additions and 7 deletions.
51 changes: 51 additions & 0 deletions web/backend/src/main/kotlin/dev/kordex/extra/web/Route.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*

@Suppress("StringLiteralDuplication")
public abstract class Route(public val extension: String) {
public abstract val path: String

public open suspend fun delete(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}

public open suspend fun get(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}

public open suspend fun head(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}

public open suspend fun options(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}

public open suspend fun patch(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}

public open suspend fun post(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}

public open suspend fun put(call: ApplicationCall) {
call.response.header("Content-Type", "application/json")
call.respond(HttpStatusCode.MethodNotAllowed, mutableMapOf("error" to "Method not allowed"))
}
}
16 changes: 14 additions & 2 deletions web/backend/src/main/kotlin/dev/kordex/extra/web/WebExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@
package dev.kordex.extra.web

import com.kotlindiscord.kord.extensions.extensions.Extension
import dev.kordex.extra.web.config.WebServerConfig
import dev.kordex.extra.web.server.WebServer

public class WebExtension : Extension() {
public class WebExtension(private val config: WebServerConfig) : Extension() {
override val name: String = "kordex-web"

public lateinit var server: WebServer

override suspend fun setup() {
TODO("Not yet implemented")
server = WebServer(config)

server.start()
}

override suspend fun unload() {
if (this::server.isInitialized) {
server.stop()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web.events

import com.kotlindiscord.kord.extensions.events.KordExEvent
import dev.kordex.extra.web.server.WebServer

public class WebServerStartEvent(public val server: WebServer) : KordExEvent
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web.events

import com.kotlindiscord.kord.extensions.events.KordExEvent

public class WebServerStopEvent : KordExEvent
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web.routes

import com.kotlindiscord.kord.extensions.events.ExtensionStateEvent
import com.kotlindiscord.kord.extensions.extensions.ExtensionState
import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent
import dev.kordex.extra.web.Route
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.util.pipeline.*

public class RouteRegistry : KordExKoinComponent {
private val routes: MutableMap<String, Route> = mutableMapOf()

public suspend fun handle(verb: Verb, context: PipelineContext<Unit, ApplicationCall>) {
val call = context.call

val path = call.parameters.getAll("path")
?.joinToString("/")

val route = routes[path]
?: return call.respond(HttpStatusCode.NotFound)

when (verb) {
Verb.DELETE -> route.delete(call)
Verb.GET -> route.get(call)
Verb.HEAD -> route.head(call)
Verb.OPTIONS -> route.options(call)
Verb.PATCH -> route.patch(call)
Verb.POST -> route.post(call)
Verb.PUT -> route.put(call)
}
}

public fun handleExtensionState(event: ExtensionStateEvent) {
if (event.state == ExtensionState.UNLOADING) {
val toRemove = routes.filter { it.value.extension == event.extension.name }

toRemove.forEach { routes.remove(it.key) }
}
}

public fun add(route: Route): Boolean {
val path = "${route.extension}/${route.path}"

if (path in routes) {
return false
}

routes[path] = route

return true
}

public fun remove(route: Route): Route? {
val path = "${route.extension}/${route.path}"

return routes.remove(path)
}

public fun removeAll() {
routes.clear()
}
}
19 changes: 19 additions & 0 deletions web/backend/src/main/kotlin/dev/kordex/extra/web/routes/Verb.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web.routes

import io.ktor.http.*

public sealed class Verb(public val method: HttpMethod) {
public data object DELETE : Verb(HttpMethod.Delete)
public data object GET : Verb(HttpMethod.Get)
public data object HEAD : Verb(HttpMethod.Head)
public data object OPTIONS : Verb(HttpMethod.Options)
public data object PATCH : Verb(HttpMethod.Patch)
public data object POST : Verb(HttpMethod.Post)
public data object PUT : Verb(HttpMethod.Put)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@

package dev.kordex.extra.web.server

import com.kotlindiscord.kord.extensions.ExtensibleBot
import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent
import dev.kordex.extra.web.config.ForwardedHeaderMode
import dev.kordex.extra.web.config.ForwardedHeaderStrategy
import dev.kordex.extra.web.config.WebServerConfig
import dev.kordex.extra.web.events.WebServerStartEvent
import dev.kordex.extra.web.events.WebServerStopEvent
import dev.kordex.extra.web.routes.RouteRegistry
import dev.kordex.extra.web.routes.Verb
import dev.kordex.extra.web.websockets.WebsocketRegistry
import io.ktor.http.*
import io.ktor.serialization.kotlinx.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.serialization.kotlinx.xml.*
import io.ktor.server.application.*
Expand All @@ -23,11 +31,21 @@ import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import kotlinx.serialization.json.Json
import org.koin.core.component.inject
import kotlin.time.Duration.Companion.seconds

private val CORS_SCHEMES = listOf("http", "https", "ws", "wss")

public class WebServer(private val config: WebServerConfig) {
public class WebServer(private val config: WebServerConfig) : KordExKoinComponent {
private val bot: ExtensibleBot by inject()

public val routeRegistry: RouteRegistry = RouteRegistry()
public val wsRegistry: WebsocketRegistry = WebsocketRegistry()

public var running: Boolean = false
private set

private val server = embeddedServer(Netty, port = config.port) {
install(ContentNegotiation) {
json()
Expand All @@ -47,10 +65,13 @@ public class WebServer(private val config: WebServerConfig) {

allowHeader(HttpHeaders.ContentType)

allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Head)
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Patch)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
}

when (config.forwardedHeaderMode) {
Expand Down Expand Up @@ -93,6 +114,8 @@ public class WebServer(private val config: WebServerConfig) {
// For some reason, ktor uses Java durations instead of Kotlin ones for the easy properties?
pingPeriodMillis = 15.seconds.inWholeMilliseconds
timeoutMillis = 15.seconds.inWholeMilliseconds

contentConverter = KotlinxWebsocketSerializationConverter(Json.Default)
}

routing {
Expand All @@ -103,15 +126,26 @@ public class WebServer(private val config: WebServerConfig) {
}
}

public fun start() {
public suspend fun start() {
server.start(wait = false)

running = true

bot.send(WebServerStartEvent(this))
}

public fun stop() {
public suspend fun stop() {
routeRegistry.removeAll()
wsRegistry.removeAll()

server.stop(
gracePeriodMillis = 0,
timeoutMillis = 0
)

running = false

bot.send(WebServerStopEvent())
}

private fun Routing.setup() {
Expand All @@ -126,6 +160,42 @@ public class WebServer(private val config: WebServerConfig) {
}
}

route("/api/extensions/{path...}") {
delete {
routeRegistry.handle(Verb.DELETE, this)
}

get {
routeRegistry.handle(Verb.GET, this)
}

head {
routeRegistry.handle(Verb.HEAD, this)
}

options {
routeRegistry.handle(Verb.OPTIONS, this)
}

patch {
routeRegistry.handle(Verb.PATCH, this)
}

post {
routeRegistry.handle(Verb.POST, this)
}

put {
routeRegistry.handle(Verb.PUT, this)
}
}

route("/ws/extensions/{path...}") {
webSocket {
wsRegistry.handle(this)
}
}

// TODO: Other routes
}
}
19 changes: 19 additions & 0 deletions web/backend/src/main/kotlin/dev/kordex/extra/web/utils/_Builder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web.utils

import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
import dev.kordex.extra.web.WebExtension
import dev.kordex.extra.web.config.WebServerConfig

public fun ExtensibleBotBuilder.ExtensionsBuilder.web(builder: WebServerConfig.() -> Unit) {
val config = WebServerConfig()

builder(config)

add { WebExtension(config) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.kordex.extra.web.utils

import com.kotlindiscord.kord.extensions.events.EventContext
import dev.kordex.extra.web.Route
import dev.kordex.extra.web.events.WebServerStartEvent
import dev.kordex.extra.web.websockets.WebsocketBuilder
import dev.kordex.extra.web.websockets.WebsocketBuilderFun

public fun EventContext<WebServerStartEvent>.route(route: Route) {
if (!event.server.routeRegistry.add(route)) {
error("Route at ${route.path} for extension ${eventHandler.extension.name} already exists.")
}
}

public fun EventContext<WebServerStartEvent>.websocket(path: String, body: WebsocketBuilderFun) {
val socketBuilder = WebsocketBuilder(eventHandler.extension.name, body)

if (!event.server.wsRegistry.add(path, socketBuilder)) {
error("Websocket at $path for extension ${eventHandler.extension.name} already exists.")
}
}
Loading

0 comments on commit a5a8feb

Please sign in to comment.