package xyz.maxwellj.chookchat import io.javalin.Javalin import io.javalin.websocket.WsContext import io.javalin.http.UploadedFile 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') } object config { var address = "" var port = "" var security = "" var serviceName = "" fun getConfig() { address = "" port = "" security = "" serviceName = "" val configFile = File("chookchat.config") try { val config = configFile.readLines() var type = "" var isEditing = 0 for (line in config) { for (char in line) { if (char == ':') { isEditing = 1 } else if (char == ';') { isEditing = 0 type = "" } else { if (isEditing == 0) { type += char } else if (isEditing == 1) if (type == "address") { address += char } else if (type == "port") { port += char } else if (type == "security") { security += char } else if (type == "serviceName") { serviceName += char } } } } } catch (e: Exception) { println("Something went wrong :/ Here's the error: $e") } } } object WsSessionManager { val peopleOnline = mutableListOf() val sessionsList = mutableListOf() val sessions = ConcurrentHashMap() val sessionIds = ConcurrentHashMap() val userSessions = ConcurrentHashMap() val roomList = mutableListOf() val userRooms = ConcurrentHashMap() val roomUsers = ConcurrentHashMap>() init { createRoom("general") fixedRateTimer("websocket-ping", period = 5000) { sendPing() } } fun createRoom(roomName: String): Boolean { if (roomList.contains(roomName)) { return false } roomList.add(roomName) roomUsers[roomName] = mutableListOf() return true } fun joinRoom(username: String, roomName: String): Boolean { if (!roomList.contains(roomName)) { createRoom(roomName) } val currentRoom = userRooms[username] if (currentRoom != null) { roomUsers[currentRoom]?.remove(username) val leftMessage = JSONObject().apply { put("type", "roomLeave") put("username", "system") put("room", currentRoom) put("content", "$username left the room") } broadcastToRoom(currentRoom, leftMessage.toString()) broadcastRoomUsers(currentRoom) } userRooms[username] = roomName roomUsers[roomName]?.add(username) broadcastRoomUsers(roomName) return true } fun getRoomUsers(roomName: String): List { return roomUsers[roomName] ?: listOf() } fun broadcastRoomUsers(roomName: String) { val usersInRoom = roomUsers[roomName] ?: listOf() val processedData = JSONObject().apply { put("type", "roomUsers") put("username", "system") put("room", roomName) put("content", usersInRoom.joinToString(", ")) } broadcastToRoom(roomName, processedData.toString(), false) } fun broadcastOnlineUsers() { val processedData = JSONObject().apply { put("type", "users") put("username", "system") put("content", peopleOnline.joinToString(", ")) } broadcast(processedData.toString()) } fun broadcastToRoom(roomName: String, message: String, cleanupDeadSessions: Boolean = true) { val deadSessions = mutableListOf() sessions.keys.forEach { ctx -> try { if (ctx.session.isOpen) { val sessionId = sessions[ctx] if (sessionId != null) { val username = userSessions.entries.find { it.value == sessionId }?.key if (username != null && userRooms[username] == roomName) { ctx.send(message) } } } else { deadSessions.add(ctx) } } catch (e: Exception) { println("Error broadcasting to session: ${e.message}") deadSessions.add(ctx) } } if (cleanupDeadSessions) { deadSessions.forEach { removeSessionWithoutBroadcast(it) } } } private fun removeSessionWithoutBroadcast(ctx: WsContext) { try { val sessionId = sessions[ctx] if (sessionId != null) { userSessions.entries.find { it.value == sessionId }?.let { entry -> val username = entry.key val room = userRooms[username] if (room != null) { roomUsers[room]?.remove(username) } peopleOnline.remove(username) userSessions.remove(username) userRooms.remove(username) } sessionsList.remove(sessionId) sessions.remove(ctx) sessionIds.remove(sessionId) } } catch (e: Exception) { println("Error removing session without broadcast: ${e.message}") } } fun handleUserLogin(username: String) { if (!peopleOnline.contains(username)) { peopleOnline.add(username) if (!userRooms.containsKey(username)) { joinRoom(username, "general") } broadcastOnlineUsers() } } 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) } } deadSessions.forEach { removeSession(it) } } fun addSession(ctx: WsContext) { try { val sessionId = UUID.randomUUID().toString() sessionsList.add(sessionId) 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) { userSessions.entries.find { it.value == sessionId }?.let { entry -> val username = entry.key val room = userRooms[username] if (room != null) { roomUsers[room]?.remove(username) broadcastRoomUsers(room) } peopleOnline.remove(username) userSessions.remove(username) userRooms.remove(username) } 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) } } deadSessions.forEach { removeSession(it) } } fun getSessionCount(): Int = sessions.size fun getUserRoom(username: String): String? { return userRooms[username] } fun getRooms(): List { return roomList } } 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 chat!") } return processedData.toString() } if (jsonInputData.getString("type") == "joinRoom") { val username = jsonInputData.getString("username") val roomName = jsonInputData.getString("room") if (!jsonInputData.has("token")) { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "Authentication required") } return processedData.toString() } val success = WsSessionManager.joinRoom(username, roomName) if (success) { val processedData = JSONObject().apply { put("type", "roomJoin") put("username", "system") put("room", roomName) put("content", "$username just joined the room!") } return processedData.toString() } else { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "Failed to join room: $roomName") } return processedData.toString() } } if (jsonInputData.getString("type") == "getUsersInRoom") { val username = jsonInputData.getString("username") val roomName = jsonInputData.getString("room") if (!jsonInputData.has("token")) { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "Authentication required") } return processedData.toString() } val usersInRoom = WsSessionManager.getRoomUsers(roomName) val processedData = JSONObject().apply { put("type", "roomUsers") put("username", "system") put("room", roomName) put("content", usersInRoom.joinToString(", ")) } return processedData.toString() } if (jsonInputData.getString("type") == "createRoom") { val username = jsonInputData.getString("username") val roomName = jsonInputData.getString("room") if (!jsonInputData.has("token")) { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "Authentication required") } return processedData.toString() } val success = WsSessionManager.createRoom(roomName) if (success) { WsSessionManager.joinRoom(username, roomName) val processedData = JSONObject().apply { put("type", "roomCreated") put("username", "system") put("room", roomName) put("content", "Room '$roomName' created and joined!") } return processedData.toString() } else { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "Room '$roomName' already exists!") } return processedData.toString() } } val username = jsonInputData.getString("username") val room = WsSessionManager.getUserRoom(username) ?: "general" val processedData = JSONObject().apply { put("type", jsonInputData.getString("type")) put("username", username) put("room", room) put("content", jsonInputData.getString("content")) } return processedData.toString() } fun handleSentMessage(inputData: String): String { println("API request received: $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 type = jsonInputData.getString("type") if (jsonInputData.has("type")) { val type = jsonInputData.getString("type") if (type == "joinRoom" || type == "createRoom") { val userDatabaseParser = BufferedReader(File("userDatabase").reader()) var userLine = "" userDatabaseParser.forEachLine { line -> if (line.contains(username)) { userLine = line } } userDatabaseParser.close() if (userLine == "") { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "unknown-account") } return processedData.toString() } var tokenInDatabase = "" var currentStage = 0 for (char in userLine) { if (char == ':') { currentStage++ } if (currentStage == 1) { tokenInDatabase += char } } tokenInDatabase = tokenInDatabase.replace(":", "") if (token != tokenInDatabase) { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "invalid-token") } return processedData.toString() } return "Success" } } val userDatabaseParser = BufferedReader(File("userDatabase").reader()) var lineNumber = 1 var userLine = "" 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() } if (token != tokenInDatabase) { val processedData = JSONObject().apply { put("type", "error") put("username", "system") put("content", "invalid-token") } return processedData.toString() } if (content != "") { if (type != "message") { return "Success" } val room = WsSessionManager.getUserRoom(username) ?: "general" val roomDirectory = File("roomChats") if (!roomDirectory.exists()) { roomDirectory.mkdir() } val roomChatHistory = File("roomChats/$room.txt") roomChatHistory.appendText("$username: $content ${System.lineSeparator()}") val chatHistory = File("chatHistory") chatHistory.appendText("$username: $content [Room: $room] ${System.lineSeparator()}") return "Success" } else { return "No data provided" } } fun getRoomChatHistory(roomName: String): String { val roomChatFile = File("roomChats/$roomName.txt") if (roomChatFile.exists()) { return roomChatFile.readText() } return "" } fun buildHTML(): String { try { config.getConfig() val htmlFile = File("resources/index.html") val html = htmlFile.readLines() var editedhtml = "" for (line in html) { if (line == """
""") { editedhtml += """
""" } else if (line == """
""") { editedhtml += """
""" } else if (line == """ """ && config.security == "false") { editedhtml += """ """ } else if (line == """

Chookchat

""") { editedhtml += """

${config.serviceName}

""" } else if (line == """ """) { val eggsFile = File("chookchat.eggs.config") val eggs = eggsFile.readLines() for (line in eggs) { val eggHTMLFile = File("eggs/$line/index.html") if (eggHTMLFile.exists()) { val eggHTML = eggHTMLFile.readText() editedhtml += eggHTML } } } else { editedhtml += line } } return(editedhtml) } catch (e: Exception) { println(e) return("There was an error! If you're the server's admin, here are the details: $e") } return("dingus") } fun buildJS(): String { try { val eggsFile = File("chookchat.eggs.config") val eggs = eggsFile.readLines() val jsFile = File("resources/index.js") val js = jsFile.readLines() var editedJS = "" for (line in js) { if (line == " // Egg message logic") { for (line in eggs) { val eggJSMessageFile = File("eggs/$line/message.js") if (eggJSMessageFile.exists()) { val eggJSMessage = eggJSMessageFile.readText() editedJS += eggJSMessage } } } else { editedJS += "$line\n" } } for (line in eggs) { val eggJSFile = File("eggs/$line/index.js") if (eggJSFile.exists()) { val eggJS = eggJSFile.readText() editedJS += eggJS } } return(editedJS) } catch (e: Exception) { println(e) return("console.log(`There was an error! If you're the server's admin, here are the details: $e`)") } return("dingus") } 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 createAccount(inputData: String): String { println("Account creation request recieved: $inputData") 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 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 main(args: Array) { WsSessionManager.peopleOnline.clear() WsSessionManager.sessionsList.clear() WsSessionManager.roomList.clear() WsSessionManager.createRoom("general") val roomDirectory = File("roomChats") if (!roomDirectory.exists()) { roomDirectory.mkdir() } val app = Javalin.create { config -> config.staticFiles.add("/public") } .get("/") { ctx -> ctx.html(buildHTML()) } .get("/index.js") { ctx -> ctx.result(buildJS()) } .get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content"))) } .get("/api/rooms") { ctx -> val rooms = WsSessionManager.getRooms() val roomsJson = JSONArray(rooms) val processedData = JSONObject().apply { put("type", "roomsList") put("username", "system") put("content", roomsJson.toString()) } ctx.result(processedData.toString()) } .get("/api/room/{roomName}/history") { ctx -> val roomName = ctx.pathParam("roomName") ctx.result(getRoomChatHistory(roomName)) } .post("/api/upload") { ctx -> val uploadedFiles = ctx.uploadedFiles() if (uploadedFiles.isEmpty()) { ctx.status(400).result("No files uploaded") return@post } val uploadedFile = uploadedFiles[0] val originalFilename = uploadedFile.filename() val uuid = UUID.randomUUID().toString() val fileExtension = originalFilename.substringAfterLast(".", "") val baseFilename = originalFilename.substringBeforeLast(".") val newFilename = "${baseFilename}_${uuid}${if (fileExtension.isNotEmpty()) ".$fileExtension" else ""}" val filePath = Paths.get("uploads", newFilename) Files.copy(uploadedFile.content(), filePath) val room = if (ctx.formParam("room") != null) ctx.formParam("room") else "general" val processedData = JSONObject().apply { put("type", "fileStatus") put("username", "system") put("content", "success") } ctx.result(processedData.toString()) val processedData2 = JSONObject().apply { put("type", "file") put("username", "system") put("room", room) put("content", "https://maxwellj.xyz/chookchat/uploads/$newFilename") } if (room != null && room != "general") { WsSessionManager.broadcastToRoom(room, processedData2.toString()) } else { WsSessionManager.broadcast(processedData2.toString()) } } .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) try { val jsonMessage = JSONObject(messageContent) if (jsonMessage.has("room")) { val room = jsonMessage.getString("room") if (WsSessionManager.roomList.contains(room)) { WsSessionManager.broadcastToRoom(room, messageContent) } else { WsSessionManager.createRoom(room) WsSessionManager.broadcastToRoom(room, messageContent) } } else { WsSessionManager.broadcastToRoom("general", messageContent) } } catch (e: Exception) { println("Error in broadcasting message: ${e.message}") WsSessionManager.broadcastToRoom("general", 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") } }