This repository has been archived on 2026-07-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
chookchat-old/server/src/main/kotlin/Main.kt
2025-03-14 08:44:06 +11:00

881 lines
29 KiB
Kotlin

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<String>()
val sessionsList = mutableListOf<String>()
val sessions = ConcurrentHashMap<WsContext, String>()
val sessionIds = ConcurrentHashMap<String, WsContext>()
val userSessions = ConcurrentHashMap<String, String>()
val roomList = mutableListOf<String>()
val userRooms = ConcurrentHashMap<String, String>()
val roomUsers = ConcurrentHashMap<String, MutableList<String>>()
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<String> {
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<WsContext>()
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<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)
}
}
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<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)
}
}
deadSessions.forEach { removeSession(it) }
}
fun getSessionCount(): Int = sessions.size
fun getUserRoom(username: String): String? {
return userRooms[username]
}
fun getRooms(): List<String> {
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 == """ <input type="text" id="serverUrl" value="bobcompass.online" placeholder="Server URL"><br>""") {
editedhtml += """ <input type="text" id="serverUrl" value="${config.address}" placeholder="Server URL"><br>"""
} else if (line == """ <input type="text" id="serverPort" value="443" placeholder="Server Port"><br>""") {
editedhtml += """ <input type="text" id="serverPort" value="${config.port}" placeholder="Server Port"><br>"""
} else if (line == """ <input type="checkbox" id="securityStatus" checked>""" && config.security == "false") {
editedhtml += """ <input type="checkbox" id="securityStatus">"""
} else if (line == """ <h3>Chookchat</h3>""") {
editedhtml += """ <h3>${config.serviceName}</h3>"""
} else if (line == """ <!-- Eggs Start Here -->""") {
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<String>) {
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")
}
}