diff --git a/server/build.gradle.kts b/server/build.gradle.kts index a2c18b0..2434d13 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -5,11 +5,11 @@ plugins { } application { - mainClass.set("xyz.maxwellj.chookpen.MainKt") + mainClass.set("xyz.maxwellj.chookchat.MainKt") layout.buildDirectory.dir("distributions/") } -group = "xyz.maxwellj.chookpen" +group = "xyz.maxwellj.chookchat" version = "0.0.1" repositories { @@ -18,7 +18,7 @@ repositories { tasks.withType { manifest { - attributes["Main-Class"] = "xyz.maxwellj.chookpen.MainKt" + attributes["Main-Class"] = "xyz.maxwellj.chookchat.MainKt" } } @@ -27,6 +27,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("io.javalin:javalin:6.3.0") implementation("org.slf4j:slf4j-simple:2.0.16") + implementation("org.json:json:20230618") } tasks.test { diff --git a/server/src/main/kotlin/Main.kt b/server/src/main/kotlin/Main.kt index 5b41a15..b1536f1 100644 --- a/server/src/main/kotlin/Main.kt +++ b/server/src/main/kotlin/Main.kt @@ -1,4 +1,4 @@ -package xyz.maxwellj.chookpen +package xyz.maxwellj.chookchat import io.javalin.Javalin import io.javalin.websocket.WsContext @@ -7,6 +7,10 @@ import java.util.UUID import kotlin.concurrent.fixedRateTimer +import org.json.JSONObject +import org.json.JSONArray +import org.json.JSONException + import java.io.File import java.io.BufferedReader @@ -20,13 +24,26 @@ fun md5(input:String): String { val md = MessageDigest.getInstance("MD5") return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') } - +/* +fun removeLines(fileName: String, lineNumber: String) { + require(!fileName.isEmpty() && startLine >= 1 && numLines >= 1) + val f = File(fileName) + var lines = f.readLines() + if (startLine > size) { + println("The starting line is beyond the length of the file") + return + } + lines = lines.take(startLine - 1) + lines.drop(startLine + n - 1) + val text = lines.joinToString(System.lineSeparator()) + f.writeText(text) +} +*/ object WsSessionManager { - var peopleOnline = mutableListOf("") - var sessionsList = mutableListOf("") - - private val sessions = ConcurrentHashMap() - private val sessionIds = ConcurrentHashMap() + val peopleOnline = mutableListOf("") + val sessionsList = mutableListOf("") + val sessions = ConcurrentHashMap() + val sessionIds = ConcurrentHashMap() + val userSessions = ConcurrentHashMap() init { fixedRateTimer("websocket-ping", period = 5000) { @@ -54,18 +71,25 @@ object WsSessionManager { } fun broadcastOnlineUsers() { - broadcast("!users:{${peopleOnline.joinToString(",")}}") + val processedData = JSONObject().apply { + put("type", "users") + put("username", "system") + put("content", peopleOnline.joinToString(", ")) + } + broadcast(processedData.toString()) } fun handleUserLogin(username: String) { - peopleOnline += username - broadcastOnlineUsers() + if (!peopleOnline.contains(username)) { + peopleOnline.add(username) + broadcastOnlineUsers() + } } fun addSession(ctx: WsContext) { try { val sessionId = UUID.randomUUID().toString() - sessionsList += sessionId + sessionsList.add(sessionId) // Changed from += to add() sessions[ctx] = sessionId sessionIds[sessionId] = ctx } catch (e: Exception) { @@ -77,8 +101,13 @@ object WsSessionManager { try { val sessionId = sessions[ctx] if (sessionId != null) { - peopleOnline.removeAt(sessionsList.indexOf(sessionId)) - sessionsList.removeAt(sessionsList.indexOf(sessionId)) + // Find and remove the username associated with this session + userSessions.entries.find { it.value == sessionId }?.let { entry -> + peopleOnline.remove(entry.key) + userSessions.remove(entry.key) + } + + sessionsList.remove(sessionId) sessions.remove(ctx) sessionIds.remove(sessionId) broadcastOnlineUsers() @@ -88,6 +117,13 @@ object WsSessionManager { } } + fun associateUserWithSession(username: String, ctx: WsContext) { + val sessionId = sessions[ctx] + if (sessionId != null) { + userSessions[username] = sessionId + } + } + fun broadcast(message: String) { val deadSessions = mutableListOf() @@ -111,69 +147,36 @@ object WsSessionManager { fun getSessionCount(): Int = sessions.size } -fun extractMessageContent(inputData: String): String { - var username = "" - var message = "" - var dataType = "" - var isParsingData = 0 - - for (char in inputData) { - if (char == ':') { - isParsingData = 1 - } else if (isParsingData == 1) { - if (char == '}') { - isParsingData = 0 - dataType = "" - } else if (char != '{') { - if (dataType == "username") { - username += char - } else if (dataType == "message") { - message += char - } - } - } else { - dataType += char +fun extractMessageContent(inputData: String, ctx: WsContext): String { + val jsonInputData = JSONObject(inputData) + if (jsonInputData.getString("type") == "connect") { + val username = jsonInputData.getString("username") + WsSessionManager.associateUserWithSession(username, ctx) + WsSessionManager.handleUserLogin(username) + val processedData = JSONObject().apply { + put("type", "connect") + put("username", "system") + put("content", "${jsonInputData.getString("username")} just joined the room!") } + return(processedData.toString()) } - - return("$username: $message") + val processedData = JSONObject().apply { + put("type", jsonInputData.getString("type")) + put("username", jsonInputData.getString("username")) + put("content", jsonInputData.getString("content")) + } + return(processedData.toString()) } fun handleSentMessage(inputData: String): String { println("API request recieved: $inputData") - // Parse data sent to the server by client - var username = "" - var token = "" - var message = "" - var dataType = "" - var command = "" - var commandArg = "" - var isParsingData = 0 - for (char in inputData) { - val character = char - if (character == ':') { - isParsingData = 1 - } else if (isParsingData == 1) { - if (character == '}') { - isParsingData = 0 - dataType = "" - } else if (character != '{') { - if (dataType == "username") { - username += character - } else if (dataType == "token") { - token += character - } else if (dataType == "message") { - message += character - } else if (dataType == "command") { - command += character - } else if (dataType == "commandArg") { - commandArg += character - } - } - } else { - dataType += character - } - } + var jsonInputData: JSONObject + try {jsonInputData = JSONObject(inputData)} catch (error: JSONException){return(error.toString())} + + val username = jsonInputData.getString("username") + val token = jsonInputData.getString("token") + val content = jsonInputData.getString("content") + val userDatabaseParser = BufferedReader(File("userDatabase").reader()) var lineNumber = 1 var userLine = "" @@ -188,12 +191,18 @@ fun handleSentMessage(inputData: String): String { userDatabaseParser.close() if (userLine == "") { - return("That account does not exist on this server.") + val processedData = JSONObject().apply { + put("type", "error") + put("username", "system") + put("content", "unknown-account") + } + return(processedData.toString()) } var usernameInDatabase = "" var tokenInDatabase = "" var saltInDatabase = "" + var banStatus = "" var currentStage = 0 for (char in userLine) { if (char == ':') { @@ -205,103 +214,46 @@ fun handleSentMessage(inputData: String): String { tokenInDatabase += char } else if (currentStage == 2) { saltInDatabase += char + } else if (currentStage == 3) { + banStatus += char } } tokenInDatabase = tokenInDatabase.replace(":", "") saltInDatabase = saltInDatabase.replace(":", "") + banStatus = banStatus.replace(":", "") + if (banStatus == "1") { + val processedData = JSONObject().apply { + put("type", "error") + put("username", "system") + put("content", "banned") + } + return(processedData.toString()) + } val tokenWithSalt = (md5(token + saltInDatabase)) /*println(saltInDatabase) println(tokenWithSalt) if (tokenWithSalt != tokenInDatabase) {*/ if (token != tokenInDatabase) { - return("Invalid token! Please try putting in your password right") + val processedData = JSONObject().apply { + put("type", "error") + put("username", "system") + put("content", "invalid-token") + } + return(processedData.toString()) } // Make the message to respond to the client val chatHistoryView = File("chatHistory") var fullMessage = "" - if (message != "") { - fullMessage = "${chatHistoryView.readText()}$username: $message" + if (content != "") { + fullMessage = "${chatHistoryView.readText()}$username: $content" // Add the client's message to the chat history val chatHistory = File("chatHistory") - chatHistory.appendText("$username: $message ${System.lineSeparator()}") - message = "" + chatHistory.appendText("$username: $content ${System.lineSeparator()}") return("Success") - } else if (command != "") { - if (command == "sync") { - return(chatHistoryView.readText()) - } else if (command == "login") { - WsSessionManager.handleUserLogin(commandArg) - return("Login successful") - } } else { return("No data provided") } - return("System: Welcome to Chookpen, $username!") -} - -fun syncMessages(inputData: String): String { - println("API request recieved: $inputData") - // Parse data sent to the server by client - var username = "" - var token = "" - var dataType = "" - var isParsingData = 0 - for (char in inputData) { - val character = char - if (character == ':') { - isParsingData = 1 - } else if (isParsingData == 1) { - if (character == '}') { - isParsingData = 0 - dataType = "" - } else if (character != '{') { - if (dataType == "username") { - username += character - } else if (dataType == "token") { - token += character - } - } - } else { - dataType += character - } - } - val userDatabaseParser = BufferedReader(File("userDatabase").reader()) - var lineNumber = 1 - var userLine = "" - - // Search the user database to find required information about the user - userDatabaseParser.forEachLine { line -> - if (line.contains(username)) { - userLine = line - } - lineNumber++ - } - userDatabaseParser.close() - - if (userLine == "") { - return("Account not found") - } - - var usernameInDatabase = "" - var tokenInDatabase = "" - var currentStage = 0 - for (char in userLine) { - if (char == ':') { - currentStage ++ - } - if (currentStage == 0) { - usernameInDatabase += char - } else if (currentStage == 1) { - tokenInDatabase += char - } - } - tokenInDatabase = tokenInDatabase.replace(":", "") - if (token != tokenInDatabase) { - return("Invalid token") - } - // Send back message history - val chatHistoryView = File("chatHistory") - return(chatHistoryView.readText()) + return("Chookchat") } fun createAccount(inputData: String): String { @@ -341,7 +293,12 @@ fun createAccount(inputData: String): String { var response = "" userDatabaseParser.forEachLine { line -> if (line.contains(username)) { - response = "Username already exists" + val processedData = JSONObject().apply { + put("type", "error") + put("username", "system") + put("content", "username-taken") + } + response = processedData.toString() } lineNumber++ } @@ -350,101 +307,47 @@ fun createAccount(inputData: String): String { } userDatabaseParser.close() if (username == "") { - return("No username") + val processedData = JSONObject().apply { + put("type", "error") + put("username", "system") + put("content", "no-username") + } + return(processedData.toString()) } if (token == "") { - return("No token") + val processedData = JSONObject().apply { + put("type", "error") + put("username", "system") + put("content", "no-token") + } + return(processedData.toString()) } val userDatabaseFile = File("userDatabase") userDatabaseFile.appendText("${System.lineSeparator()}$username:$token") - return("Success") + val processedData = JSONObject().apply { + put("type", "success") + put("username", "system") + put("content", "success") + } + return(processedData.toString()) } -fun authKey(inputData: String): String { - println("API request recieved: $inputData") - - // Parse data sent to the server by client - var username = "" - var token = "" - var authKey = "" - var dataType = "" - var isParsingData = 0 - for (char in inputData) { - val character = char - if (character == ':') { - isParsingData = 1 - } else if (isParsingData == 1) { - if (character == '}') { - isParsingData = 0 - dataType = "" - } else if (character != '{') { - if (dataType == "username") { - username += character - } else if (dataType == "token") { - token += character - } else if (dataType == "authkey") { - authKey += character - } - } - } else { - dataType += character - } - } - val userDatabaseParser = BufferedReader(File("userDatabase").reader()) - var lineNumber = 1 - var userLine = "" - - // Search the user database to find required information about the user - userDatabaseParser.forEachLine { line -> - if (line.contains(username)) { - userLine = line - } - lineNumber++ - } - userDatabaseParser.close() - if (userLine == "") { - return("Account not found") - } - - var usernameInDatabase = "" - var tokenInDatabase = "" +fun handleServerCommand(command: String): String { + val commandArgs = mutableListOf("") + commandArgs.drop(1) var currentStage = 0 - for (char in userLine) { - if (char == ':') { - currentStage ++ - } - if (currentStage == 0) { - usernameInDatabase += char - } else if (currentStage == 1) { - tokenInDatabase += char - } - } - tokenInDatabase = tokenInDatabase.replace(":", "") - if (token != tokenInDatabase) { - return("Invalid token") - } - if (authKey == "") { - return("No auth key provided") - } - // Make the message to respond to the client - val chatHistoryView = File("chatHistory") - var fullMessage = "" - if (authKey != "") { - fullMessage = "encryptionKey:$username:$authKey" - authKey = "" - } else { - fullMessage = "${chatHistoryView.readText()}" - } - val response = if (inputData.isNotEmpty()) { - fullMessage - } else { - "No data provided" - } - // Send the message to the client - return("Success") + for (char in command) { + if (char == ' ') { + currentStage ++ + commandArgs += "" + } else { + commandArgs[currentStage] += char + } + } + return("I'm not sure how to ${commandArgs.toString()}") } fun main(args: Array) { @@ -455,18 +358,7 @@ fun main(args: Array) { }.get("/") { ctx -> ctx.redirect("/index.html") } - .get("/api/send/{content}") { ctx -> - val result = handleSentMessage(ctx.pathParam("content")) - if (result == "Success") { - val messageContent = extractMessageContent(ctx.pathParam("content")) - WsSessionManager.broadcast(messageContent) - } - ctx.result(result) - } .get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content")))} - .get("/api/syncmessages/{content}") { ctx -> ctx.result(syncMessages(ctx.pathParam("content")))} - .get("/api/authkey/{content}") { ctx -> ctx.result(authKey(ctx.pathParam("content")))} - .ws("/api/websocket") { ws -> ws.onConnect { ctx -> WsSessionManager.addSession(ctx) @@ -487,7 +379,7 @@ fun main(args: Array) { println("Error sending error message: ${e.message}") } } else { - val messageContent = extractMessageContent(ctx.message()) + val messageContent = extractMessageContent(ctx.message(), ctx) WsSessionManager.broadcast(messageContent) } } @@ -495,6 +387,9 @@ fun main(args: Array) { } } .start(7070) - + println("Type a command for the server") + while (1 == 1) { + println(handleServerCommand(readln())) + } } diff --git a/server/src/main/resources/public/gradient.css b/server/src/main/resources/public/gradient.css new file mode 120000 index 0000000..bf6fef7 --- /dev/null +++ b/server/src/main/resources/public/gradient.css @@ -0,0 +1 @@ +../../../../../client-web/gradient.css \ No newline at end of file