package xyz.maxwellj.chookchat import io.javalin.Javalin import io.javalin.websocket.WsContext import java.util.concurrent.ConcurrentHashMap 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 import java.math.BigInteger import java.security.MessageDigest import java.nio.file.Paths import java.nio.file.Files 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 { val peopleOnline = mutableListOf("") val sessionsList = mutableListOf("") val sessions = ConcurrentHashMap() val sessionIds = ConcurrentHashMap() val userSessions = ConcurrentHashMap() init { fixedRateTimer("websocket-ping", period = 5000) { sendPing() } } private fun sendPing() { val deadSessions = mutableListOf() sessions.keys.forEach { ctx -> try { if (ctx.session.isOpen) { ctx.send("ping") } else { deadSessions.add(ctx) } } catch (e: Exception) { println("Error sending ping: ${e.message}") deadSessions.add(ctx) } } // Clean up any dead sessions deadSessions.forEach { removeSession(it) } } fun broadcastOnlineUsers() { val processedData = JSONObject().apply { put("type", "users") put("username", "system") put("content", peopleOnline.joinToString(", ")) } broadcast(processedData.toString()) } fun handleUserLogin(username: String) { if (!peopleOnline.contains(username)) { peopleOnline.add(username) broadcastOnlineUsers() } } fun addSession(ctx: WsContext) { try { val sessionId = UUID.randomUUID().toString() sessionsList.add(sessionId) // Changed from += to add() sessions[ctx] = sessionId sessionIds[sessionId] = ctx } catch (e: Exception) { println("Error adding session: ${e.message}") } } fun removeSession(ctx: WsContext) { try { val sessionId = sessions[ctx] if (sessionId != null) { // 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() } } catch (e: Exception) { println("Error removing session: ${e.message}") } } fun associateUserWithSession(username: String, ctx: WsContext) { val sessionId = sessions[ctx] if (sessionId != null) { userSessions[username] = sessionId } } fun broadcast(message: String) { val deadSessions = mutableListOf() sessions.keys.forEach { ctx -> try { if (ctx.session.isOpen) { ctx.send(message) } else { deadSessions.add(ctx) } } catch (e: Exception) { println("Error broadcasting to session: ${e.message}") deadSessions.add(ctx) } } // Clean up any dead sessions deadSessions.forEach { removeSession(it) } } fun getSessionCount(): Int = sessions.size } 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()) } 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") 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 = "" // 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 == "") { 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 == ':') { currentStage ++ } if (currentStage == 0) { usernameInDatabase += char } else if (currentStage == 1) { 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) { 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 (content != "") { fullMessage = "${chatHistoryView.readText()}$username: $content" // Add the client's message to the chat history val chatHistory = File("chatHistory") chatHistory.appendText("$username: $content ${System.lineSeparator()}") return("Success") } else { return("No data provided") } return("Chookchat") } fun createAccount(inputData: String): String { println("Account creation request recieved: $inputData") // Parse data sent to the server by client var username = "" var token = "" var message = "" 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 == "message") { message += character } } } else { dataType += character } } val userDatabaseParser = BufferedReader(File("userDatabase").reader()) var lineNumber = 1 var userExists = 0 // Search the user database to find required information about the user var response = "" userDatabaseParser.forEachLine { line -> if (line.contains(username)) { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "username-taken") } response = processedData.toString() } lineNumber++ } if (response != "") { return(response) } userDatabaseParser.close() if (username == "") { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "no-username") } return(processedData.toString()) } if (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") val processedData = JSONObject().apply { put("type", "success") put("username", "system") put("content", "success") } return(processedData.toString()) } fun handleServerCommand(command: String): String { val commandArgs = mutableListOf("") commandArgs.drop(1) var currentStage = 0 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) { WsSessionManager.peopleOnline.removeAt(0) WsSessionManager.sessionsList.removeAt(0) val app = Javalin.create { config -> config.staticFiles.add("/public") }.get("/") { ctx -> ctx.redirect("/index.html") } .get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content")))} .ws("/api/websocket") { ws -> ws.onConnect { ctx -> WsSessionManager.addSession(ctx) } ws.onClose { ctx -> WsSessionManager.removeSession(ctx) } ws.onMessage { ctx -> when (ctx.message()) { "pong" -> {} else -> { println(ctx.message()) val successState = handleSentMessage(ctx.message()) if (successState != "Success") { try { ctx.send(successState) } catch (e: Exception) { println("Error sending error message: ${e.message}") } } else { val messageContent = extractMessageContent(ctx.message(), ctx) WsSessionManager.broadcast(messageContent) } } } } } .start(7070) try { if (args[0] == "-i") { println("Type a command for the server") while (1 == 1) { println(handleServerCommand(readln())) } } else { println("Interactive mode disabled, add -i to enable") } } catch (error: Exception) { println("Interactive mode disabled, add -i to enable") } }