chookchat/server/src/main/kotlin/Main.kt
2024-11-23 19:38:19 +11:00

404 lines
13 KiB
Kotlin

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<WsContext, String>()
val sessionIds = ConcurrentHashMap<String, WsContext>()
val userSessions = ConcurrentHashMap<String, String>()
init {
fixedRateTimer("websocket-ping", period = 5000) {
sendPing()
}
}
private fun sendPing() {
val deadSessions = mutableListOf<WsContext>()
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<WsContext>()
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<String>) {
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")
}
}