368 lines
13 KiB
Kotlin
368 lines
13 KiB
Kotlin
package nl.voidcorp.sqa
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper
|
|
import com.fasterxml.jackson.databind.SerializationFeature
|
|
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
|
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
import com.fasterxml.jackson.module.kotlin.readValue
|
|
import io.ktor.application.Application
|
|
import io.ktor.application.call
|
|
import io.ktor.application.install
|
|
import io.ktor.features.CallLogging
|
|
import io.ktor.features.ContentNegotiation
|
|
import io.ktor.html.respondHtml
|
|
import io.ktor.http.ContentType
|
|
import io.ktor.http.URLProtocol
|
|
import io.ktor.http.cio.websocket.*
|
|
import io.ktor.http.isSecure
|
|
import io.ktor.jackson.jackson
|
|
import io.ktor.response.respondRedirect
|
|
import io.ktor.response.respondText
|
|
import io.ktor.routing.get
|
|
import io.ktor.routing.routing
|
|
import io.ktor.server.engine.embeddedServer
|
|
import io.ktor.server.netty.Netty
|
|
import io.ktor.util.url
|
|
import io.ktor.websocket.DefaultWebSocketServerSession
|
|
import io.ktor.websocket.WebSockets
|
|
import io.ktor.websocket.webSocket
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.channels.mapNotNull
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.html.*
|
|
import org.slf4j.event.Level
|
|
import redis.clients.jedis.JedisPool
|
|
import redis.clients.jedis.Protocol
|
|
import java.net.URI
|
|
import java.time.Duration
|
|
import java.time.LocalDateTime
|
|
|
|
/*data class StageCookie(val stage: Int = 0)
|
|
|
|
fun Application.module() {
|
|
install(Sessions) {
|
|
header<StageCookie>("stage") {
|
|
transform(SessionTransportTransformerMessageAuthentication(SecureRandom.getInstanceStrong().generateSeed(8)))
|
|
}
|
|
}
|
|
|
|
routing {
|
|
get("/") {
|
|
val stage = call.sessions.get<StageCookie>() ?: StageCookie()
|
|
call.sessions.set(stage)
|
|
|
|
}
|
|
}
|
|
}*/
|
|
|
|
lateinit var mapper: ObjectMapper
|
|
|
|
val people = listOf(
|
|
"Dany",
|
|
"Robbin",
|
|
"Laura",
|
|
"Jonathan",
|
|
"Tim",
|
|
"Ricardo",
|
|
"Julius",
|
|
"Victor"
|
|
)
|
|
|
|
enum class Types {
|
|
ask,
|
|
answer
|
|
}
|
|
|
|
data class Message(val sender: String, val message: String, val timestamp: LocalDateTime = LocalDateTime.now())
|
|
data class RecvMsg(val message: String)
|
|
|
|
abstract class MessageStore {
|
|
private val sockets = mutableMapOf<String, MutableList<DefaultWebSocketServerSession>>()
|
|
open fun getSocketsFor(name: String): MutableList<DefaultWebSocketServerSession> {
|
|
return sockets.getOrPut(name) { mutableListOf() }
|
|
}
|
|
|
|
open fun setSocketsFor(name: String, list: MutableList<DefaultWebSocketServerSession>) {
|
|
sockets[name] = list
|
|
}
|
|
|
|
fun modifySockets(name: String, func: MutableList<DefaultWebSocketServerSession>.() -> Unit) {
|
|
val s = getSocketsFor(name)
|
|
s.func()
|
|
setSocketsFor(name, s)
|
|
}
|
|
|
|
abstract fun addMessageTo(name: String, message: Message)
|
|
|
|
abstract fun getMessagesFor(name: String): List<Message>
|
|
}
|
|
|
|
class MemoryMessageStore : MessageStore() {
|
|
private val l = mutableMapOf<String, MutableList<Message>>()
|
|
override fun addMessageTo(name: String, message: Message) {
|
|
val msgs = l.getOrPut(name) { mutableListOf() }
|
|
msgs += message
|
|
l[name] = msgs
|
|
}
|
|
|
|
override fun getMessagesFor(name: String): List<Message> {
|
|
return l.getOrPut(name) { mutableListOf() }
|
|
}
|
|
}
|
|
|
|
class RedisMessageStore(private val pool: JedisPool) : MessageStore() {
|
|
override fun addMessageTo(name: String, message: Message) {
|
|
pool.resource.use {
|
|
it.rpush(name, mapper.writeValueAsString(message))
|
|
|
|
}
|
|
}
|
|
|
|
override fun getMessagesFor(name: String): List<Message> {
|
|
val r: List<String> = pool.resource.use {
|
|
it.lrange(name, 0, -1)
|
|
}
|
|
return r.map { mapper.readValue<Message>(it) }
|
|
}
|
|
}
|
|
|
|
|
|
lateinit var mms: MessageStore
|
|
|
|
fun Application.module() {
|
|
|
|
install(ContentNegotiation) {
|
|
jackson {
|
|
this.registerModules(Jdk8Module(), JavaTimeModule())
|
|
this.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
|
mapper = this
|
|
}
|
|
}
|
|
|
|
install(WebSockets) {
|
|
pingPeriod = Duration.ofSeconds(60)
|
|
|
|
}
|
|
|
|
install(CallLogging) {
|
|
level = Level.INFO
|
|
}
|
|
|
|
routing {
|
|
get("/") {
|
|
val missing = call.parameters["missing"] != null
|
|
val invalid = call.parameters["invalid"] != null
|
|
call.respondHtml {
|
|
head {
|
|
title("SomeQuestionsAsked")
|
|
link("/css.css", "stylesheet")
|
|
}
|
|
body {
|
|
if (missing) {
|
|
p {
|
|
id = "missing"
|
|
+"You were (somehow) missing some parameters, try again..."
|
|
}
|
|
}
|
|
if (invalid) {
|
|
p {
|
|
id = "invalid"
|
|
+"Some parameters were invalid, try again"
|
|
}
|
|
}
|
|
form("/ask") {
|
|
label {
|
|
htmlFor = "name"
|
|
+"Who: "
|
|
}
|
|
select {
|
|
id = "name"
|
|
name = "name"
|
|
people.forEach {
|
|
option {
|
|
value = it
|
|
+it
|
|
}
|
|
}
|
|
}
|
|
div {
|
|
radioInput {
|
|
name = "type"
|
|
value = "ask"
|
|
checked = true
|
|
id = "askradio"
|
|
}
|
|
label {
|
|
htmlFor = "askradio"
|
|
+"Ask Questions"
|
|
}
|
|
br
|
|
radioInput {
|
|
name = "type"
|
|
value = "answer"
|
|
id = "answerradio"
|
|
}
|
|
label {
|
|
htmlFor = "answerradio"
|
|
+"Answer Questions"
|
|
}
|
|
}
|
|
button {
|
|
type = ButtonType.submit
|
|
+"Go!"
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get("/ask") {
|
|
val who = call.parameters["name"]
|
|
val what = Types.values().find { it.name == call.parameters["type"] }
|
|
if (who == null) {
|
|
call.respondRedirect("/?missing=true")
|
|
return@get
|
|
}
|
|
if (who !in people || what == null) {
|
|
call.respondRedirect("/?invalid=true")
|
|
return@get
|
|
}
|
|
|
|
call.respondHtml {
|
|
head {
|
|
title("${what.name.capitalize()}ing questions: $who")
|
|
link("/css.css", "stylesheet")
|
|
}
|
|
body {
|
|
div {
|
|
id = "send"
|
|
input {
|
|
id = "messageText"
|
|
}
|
|
button {
|
|
onClick = "sendMessage()"
|
|
+"Send!"
|
|
}
|
|
}
|
|
div {
|
|
id = "messages"
|
|
}
|
|
|
|
script {
|
|
val wsurl = call.url {
|
|
protocol = if (protocol.isSecure()) URLProtocol.WSS else URLProtocol.WS
|
|
path("/ws")
|
|
}
|
|
unsafe {
|
|
//language=JavaScript
|
|
+"""
|
|
let ws = new WebSocket("$wsurl");
|
|
ws.onmessage = function(event) {
|
|
let wsd = JSON.parse(event.data);
|
|
if (Array.isArray(wsd)){
|
|
wsd.forEach(e=>handleMessage(e))
|
|
}else{
|
|
handleMessage(wsd)
|
|
}
|
|
};
|
|
|
|
String.prototype.isEmpty = function() {
|
|
return (this.length === 0 || !this.trim());
|
|
};
|
|
|
|
document.getElementById("messageText").addEventListener("keydown", function (e) {
|
|
if (e.key === "Enter") { sendMessage(); }
|
|
}, true);
|
|
|
|
function sendMessage() {
|
|
let inp = document.getElementById("messageText").value;
|
|
if (inp.isEmpty()) return;
|
|
ws.send(JSON.stringify({message: inp}));
|
|
document.getElementById("messageText").value = "";
|
|
}
|
|
|
|
function handleMessage(wsd){
|
|
let sender = wsd.sender;
|
|
let message = wsd.message;
|
|
let dt = wsd.timestamp;
|
|
let outerP = document.createElement("P");
|
|
let senderS = document.createElement("SPAN");
|
|
let messageS = document.createElement("SPAN");
|
|
senderS.innerText = sender+": ";
|
|
messageS.innerText = message;
|
|
outerP.title = new Date(dt).toString();
|
|
outerP.appendChild(senderS);
|
|
outerP.appendChild(messageS);
|
|
document.getElementById("messages").appendChild(outerP);
|
|
}
|
|
|
|
""".trimIndent()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
webSocket("/ws") {
|
|
val who = call.parameters["name"]
|
|
val what = Types.values().find { it.name == call.parameters["type"] }
|
|
if (who == null) {
|
|
this.close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Missing name and type parameters..."))
|
|
return@webSocket
|
|
}
|
|
if (who !in people || what == null) {
|
|
this.close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Missing name and type parameters..."))
|
|
return@webSocket
|
|
}
|
|
|
|
mms.modifySockets(who) {
|
|
add(this@webSocket)
|
|
}
|
|
withContext(Dispatchers.IO) {
|
|
send(mapper.writeValueAsString(mms.getMessagesFor(who)))
|
|
}
|
|
for (frame in this.incoming.mapNotNull { it as? Frame.Text }) {
|
|
try {
|
|
val msg: RecvMsg = mapper.readValue(frame.readText())
|
|
val send = Message(what.let { if (it == Types.ask) who else "someone" }, msg.message)
|
|
mms.addMessageTo(who, send)
|
|
mms.getSocketsFor(who).forEach {
|
|
it.send(mapper.writeValueAsString(send))
|
|
}
|
|
} catch (e: Exception) {
|
|
//do nothing, we just ignore wrong messages...
|
|
}
|
|
}
|
|
|
|
mms.modifySockets(who) {
|
|
remove(this@webSocket)
|
|
}
|
|
|
|
}
|
|
|
|
get("/css.css") {
|
|
call.respondText(ContentType.Text.CSS) {
|
|
//language=CSS
|
|
"""
|
|
@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
|
|
* {
|
|
font-family: 'Roboto', sans-serif !important;
|
|
}
|
|
""".trimIndent()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
fun main() {
|
|
val host: String? = System.getenv("SQA_REDIS_HOST")
|
|
val port: Int? = System.getenv("SQA_REDIS_PORT")?.toIntOrNull()
|
|
mms = if (host != null) {
|
|
RedisMessageStore(JedisPool(host, port ?: Protocol.DEFAULT_PORT))
|
|
} else {
|
|
MemoryMessageStore()
|
|
}
|
|
embeddedServer(Netty, port = 8080, module = Application::module).start(true)
|
|
} |