Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0a029fcc23 | ||
![]() |
eea257d617 | ||
![]() |
e463d3dd2b | ||
![]() |
7ff5447388 | ||
![]() |
23fc71bd45 | ||
![]() |
90cf9f4325 | ||
![]() |
857a05b9ca | ||
![]() |
c07a2c7fce | ||
![]() |
500ac029df | ||
![]() |
83fc6052ad | ||
![]() |
dae4bb380f | ||
![]() |
78b885b22e | ||
![]() |
f69a3fb599 |
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -2,8 +2,7 @@
|
||||||
.swp
|
.swp
|
||||||
server/build
|
server/build
|
||||||
server/.gradle
|
server/.gradle
|
||||||
|
server/uploads
|
||||||
server/userDatabase
|
server/userDatabase
|
||||||
server/chatHistory
|
server/chatHistory
|
||||||
client-cli/build
|
server/roomChats
|
||||||
client-cli/.gradle
|
|
||||||
client-cli/.chookpen.profile
|
|
||||||
|
|
10
README.md
10
README.md
|
@ -1,8 +1,8 @@
|
||||||
# Chookpen - A simple messaging service
|
# Chookchat - A simple messaging service
|
||||||
|
|
||||||
## What is Chookpen?
|
## What is Chookchat?
|
||||||
|
|
||||||
Chookpen is a lightweight, secure-ish chat server implementation focused on simplicity and real-time communication. It features user authentication, persistent message history, real-time updates via WebSockets, and supports both traditional HTTP endpoints and WebSocket connections for flexible integration. It uses very little resources on a system, often using a maximum of 20mb. Chookpen is BETA SOFTWARE, and MAY BE BUGGY! Don't expect too much.
|
Chookchat is a lightweight, secure-ish chat server implementation focused on simplicity and real-time communication. It features user authentication, persistent message history and real-time updates via WebSockets. It uses very little resources on a system, often using a maximum of 20mb. Chookpen is BETA SOFTWARE, and MAY BE BUGGY! Don't expect too much.
|
||||||
|
|
||||||
## A guide to this repository
|
## A guide to this repository
|
||||||
|
|
||||||
|
@ -25,3 +25,7 @@ The build tool for compiling Chookpen. It just works!
|
||||||
The language Chookpen is coded in. How else does it work?
|
The language Chookpen is coded in. How else does it work?
|
||||||
|
|
||||||
### [OpenJDK](https://openjdk.org)
|
### [OpenJDK](https://openjdk.org)
|
||||||
|
|
||||||
|
The best way to run Chookpen. Free and open source!
|
||||||
|
|
||||||
|
There's also all the other various libraries I use (and these other libraries use) so check the code for those!
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Chookpen Client (CLI)
|
|
||||||
|
|
||||||
This is the reference client for Chookpen. It is quite a basic client. This is a good starting point if you need an example, or are building a GUI Kotlin client.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* Profile file
|
|
||||||
* Create account (buggy)
|
|
||||||
* Log into existing account
|
|
||||||
* Sync messages
|
|
||||||
* Send message
|
|
||||||
* Auto hash password
|
|
||||||
* Configure whether HTTPS is used
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
To run this client, do the following:
|
|
||||||
|
|
||||||
1. Install Java 17+ and Gradle.
|
|
||||||
2. `git clone https://git.maxwellj.xyz/max/chookpen`
|
|
||||||
3. In the client-cli directory of the git repo, run `gradle installDist`.
|
|
||||||
5. Inside build/install/bin, create a .chookpen.profile file that looks like this: ```name:password:server:port:0```
|
|
||||||
6. Run the script in build/install/bin, or the bat file if you're a crazy weird guy who uses Windows (Make sure to use the command line otherwise it'll close instantly)
|
|
||||||
7. If you'd like to send a message, add that as your argument after specifying the script.
|
|
||||||
|
|
||||||
Note: The password in .chookpen.profile is hashed by the client. If you're just testing the server, set your password to `dingus`. The hash recognised by the server for that password is `750c63441033127dccaa91c16b21614e`. Create an account with the method specified in the root README using that password.
|
|
|
@ -1,35 +0,0 @@
|
||||||
plugins {
|
|
||||||
kotlin("jvm") version "2.0.0"
|
|
||||||
application
|
|
||||||
distribution
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass.set("xyz.maxwellj.chookpen.client.MainKt")
|
|
||||||
layout.buildDirectory.dir("distributions/")
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "xyz.maxwellj.chookpen.client"
|
|
||||||
version = "0.0.2"
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Jar> {
|
|
||||||
manifest {
|
|
||||||
attributes["Main-Class"] = "xyz.maxwellj.chookpen.client.MainKt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(17)
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
|
||||||
}
|
|
||||||
rootProject.name = "chookpen.client"
|
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
package xyz.maxwellj.chookpen.client
|
|
||||||
|
|
||||||
import okhttp3.*
|
|
||||||
import java.util.Scanner
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
fun md5(input:String): String {
|
|
||||||
val md = MessageDigest.getInstance("MD5")
|
|
||||||
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
// Variables
|
|
||||||
var name = ""
|
|
||||||
var server = ""
|
|
||||||
var port = ""
|
|
||||||
var hasHTTPS = ""
|
|
||||||
var password = ""
|
|
||||||
var configFile = File("${System.getProperty("user.home")}/chookpen.profile")
|
|
||||||
|
|
||||||
var configStage = 0
|
|
||||||
for (char in configFile.readText()) {
|
|
||||||
if (char == ':') {configStage ++}
|
|
||||||
if (configStage == 0) {
|
|
||||||
name += char
|
|
||||||
} else if (configStage == 1) {
|
|
||||||
password += char
|
|
||||||
} else if (configStage == 2) {
|
|
||||||
server += char
|
|
||||||
} else if (configStage == 3) {
|
|
||||||
port += char
|
|
||||||
} else if (configStage == 4) {
|
|
||||||
hasHTTPS += char
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server = server.replace(":", "")
|
|
||||||
port = port.replace(":", "")
|
|
||||||
hasHTTPS = hasHTTPS.replace(":", "")
|
|
||||||
password = password.replace(":", "")
|
|
||||||
|
|
||||||
if (password == "x") {
|
|
||||||
println("Enter your password:")
|
|
||||||
password = readln()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
|
||||||
.pingInterval(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("ws://localhost:7070/api/websocket")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
var webSocket: WebSocket? = null
|
|
||||||
|
|
||||||
val listener = object : WebSocketListener() {
|
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
|
||||||
println(password)
|
|
||||||
println(md5(password))
|
|
||||||
println("Connection opened")
|
|
||||||
webSocket.send("username:{$name}token:{${md5(password)}}message:{Joined the room}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
||||||
println("$text")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
|
||||||
println("Connection closing: $reason")
|
|
||||||
webSocket.close(1000, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
|
||||||
println("Connection failed: ${t.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up shutdown hook for Ctrl+C handling
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
|
||||||
println("\nShutting down gracefully...")
|
|
||||||
webSocket?.close(1000, "Client shutting down")
|
|
||||||
client.dispatcher.executorService.shutdown()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize WebSocket connection
|
|
||||||
webSocket = client.newWebSocket(request, listener)
|
|
||||||
|
|
||||||
// Set up input handling
|
|
||||||
val scanner = Scanner(System.`in`)
|
|
||||||
println("Type your messages (press Enter to send, Ctrl+C to quit):")
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
val input = scanner.nextLine()
|
|
||||||
if (input.isNotEmpty()) {
|
|
||||||
webSocket?.send("username:{$name}token:{${md5(password)}}message:{$input}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Handle any input-related exceptions
|
|
||||||
println("Error reading input: ${e.message}")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
13
client-python/README.md
Normal file
13
client-python/README.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
## Chookchat Python Client
|
||||||
|
|
||||||
|
This is an example client for Chookchat, written in Python. It is very simplistic. It connects to the Websocket, and prints recieved messages.
|
||||||
|
|
||||||
|
This would be a good baseline for a GUI client with tkinter, or a bot for Chookchat linking it to other services.
|
||||||
|
|
||||||
|
Not much else to say.
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
First, `pip3 install websocket-client rel`. If you need, create a virtual environment for Python.
|
||||||
|
|
||||||
|
Then, run with `python3 client.py`
|
68
client-python/client.py
Normal file
68
client-python/client.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import websocket
|
||||||
|
import json
|
||||||
|
import _thread
|
||||||
|
import time
|
||||||
|
import rel
|
||||||
|
from google import genai
|
||||||
|
|
||||||
|
username = "(insert username here)"
|
||||||
|
token = "(insert token here)"
|
||||||
|
|
||||||
|
def joinRoom(roomName):
|
||||||
|
ws.send(json.dumps({
|
||||||
|
"type": "joinRoom",
|
||||||
|
"username": username,
|
||||||
|
"token": token,
|
||||||
|
"room": roomName,
|
||||||
|
"content": ""
|
||||||
|
}))
|
||||||
|
|
||||||
|
def sendMessage(content):
|
||||||
|
ws.send(json.dumps({
|
||||||
|
"type": "message",
|
||||||
|
"username": username,
|
||||||
|
"token": token,
|
||||||
|
"content": content
|
||||||
|
}))
|
||||||
|
|
||||||
|
def sendTyping(content):
|
||||||
|
ws.send(json.dumps({
|
||||||
|
"type": "typing",
|
||||||
|
"username": username,
|
||||||
|
"token": token,
|
||||||
|
"content": content
|
||||||
|
}))
|
||||||
|
|
||||||
|
def on_message(ws, message):
|
||||||
|
print("Message received: " + message)
|
||||||
|
if message == "ping":
|
||||||
|
ws.send("pong")
|
||||||
|
|
||||||
|
def on_error(ws, error):
|
||||||
|
print("Error:", error)
|
||||||
|
|
||||||
|
def on_close(ws, close_status_code, close_msg):
|
||||||
|
print(f"Connection closed: {close_status_code} - {close_msg}")
|
||||||
|
|
||||||
|
def on_open(ws):
|
||||||
|
print("Opening connection to Chookchat...")
|
||||||
|
joinRoom("general")
|
||||||
|
ws.send(json.dumps({
|
||||||
|
"type": "connect",
|
||||||
|
"username": username,
|
||||||
|
"token": token,
|
||||||
|
"content": username + " joined the room!"
|
||||||
|
}))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
websocket.enableTrace(True)
|
||||||
|
ws = websocket.WebSocketApp("wss://bobcompass.online/api/websocket",
|
||||||
|
on_open=on_open,
|
||||||
|
on_message=on_message,
|
||||||
|
on_error=on_error,
|
||||||
|
on_close=on_close
|
||||||
|
)
|
||||||
|
|
||||||
|
ws.run_forever(dispatcher=rel, reconnect=5, ping_interval=30, ping_timeout=10)
|
||||||
|
rel.signal(2, rel.abort)
|
||||||
|
rel.dispatch()
|
|
@ -1,43 +1,21 @@
|
||||||
/* Reset default margin and ensure full viewport coverage */
|
/* Credit to Manuel Pinto on Codepen for the code!
|
||||||
|
https://codepen.io/P1N2O/pen/pyBNzX */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
background: linear-gradient(-33deg, #721c7a, #612bd4, #3aa5c5, #30cf7f);
|
||||||
min-height: 100vh;
|
background-size: 400% 400%;
|
||||||
background: linear-gradient(
|
animation: gradient 120s ease infinite;
|
||||||
30deg,
|
height: 100vh;
|
||||||
#50C878, /* green */
|
|
||||||
#7FFFD4, /* aqua */
|
|
||||||
#87CEEB, /* light blue */
|
|
||||||
#00008B, /* dark blue */
|
|
||||||
#800080, /* purple */
|
|
||||||
#50C878, /* green */
|
|
||||||
#7FFFD4, /* aqua */
|
|
||||||
#87CEEB, /* light blue */
|
|
||||||
#00008B, /* dark blue */
|
|
||||||
#800080, /* purple */
|
|
||||||
#50C878 /* green */
|
|
||||||
);
|
|
||||||
background-size: 1000% 1000%;
|
|
||||||
animation: gradient 120s ease infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient animation keyframes */
|
|
||||||
@keyframes gradient {
|
@keyframes gradient {
|
||||||
0% {
|
0% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
100% {
|
50% {
|
||||||
background-position: 200% 50%;
|
background-position: 100% 50%;
|
||||||
}
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optional: styling for centered content */
|
|
||||||
.content {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: white;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 2rem;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
|
@ -42,61 +42,17 @@ p {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 10px 10px;
|
margin: 10px 10px;
|
||||||
flex-grow: 1; /* This makes it take up remaining space */
|
flex-grow: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.section h2 {
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box p {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
.box h3 {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
.box img {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
.box button {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bluebutton {
|
.bluebutton {
|
||||||
color: white;
|
|
||||||
font-size: 12pt;
|
|
||||||
background: rgba(0, 0, 255, 0.3);
|
background: rgba(0, 0, 255, 0.3);
|
||||||
font-family: inter;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
}
|
||||||
.greenbutton {
|
.greenbutton {
|
||||||
color: white;
|
|
||||||
font-size: 12pt;
|
|
||||||
background: rgba(0, 255, 0, 0.3);
|
background: rgba(0, 255, 0, 0.3);
|
||||||
font-family: inter;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
}
|
||||||
.redbutton {
|
.redbutton {
|
||||||
color: white;
|
|
||||||
font-size: 12pt;
|
|
||||||
background: rgba(255, 0, 0, 0.3);
|
background: rgba(255, 0, 0, 0.3);
|
||||||
font-family: inter;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
.backbutton {
|
|
||||||
color: white;
|
|
||||||
font-size: 12pt;
|
|
||||||
background: rgba(255, 0, 0, 0.3);
|
|
||||||
font-family: inter;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -121,17 +77,19 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update section styles */
|
|
||||||
.section {
|
.section {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
width: calc(100vw - 40px);
|
width: calc(100vw - 40px);
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 40px);
|
||||||
box-sizing: border-box; /* Add this to include padding in width calculation */
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update box styles */
|
|
||||||
.box {
|
.box {
|
||||||
color: white;
|
color: white;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
@ -146,40 +104,57 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Special handling for messaging box */
|
.box p {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.box h3 {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.box img {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.box button {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#messaging .box {
|
#messaging .box {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative; /* For absolute positioning of input container */
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messagebox {
|
#messagebox {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 10px 0; /* Remove horizontal margin */
|
margin: 10px 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for individual messages */
|
|
||||||
.message {
|
.message {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
float: left;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create a container for message input and send button */
|
|
||||||
.input-container {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 10px; /* Space between input and button */
|
gap: 10px;
|
||||||
/* padding: 10px;*/
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update input styles */
|
|
||||||
input {
|
input {
|
||||||
color: white;
|
color: white;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
@ -192,17 +167,253 @@ input {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Special style for message input */
|
|
||||||
#messageInput {
|
#messageInput {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
flex-grow: 1; /* Take up remaining space */
|
flex-grow: 1;
|
||||||
margin: 0; /* Remove default margins */
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update button styles */
|
.bluebutton, .greenbutton, .redbutton {
|
||||||
.bluebutton, .greenbutton, .redbutton, .backbutton {
|
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
width: auto; /* Allow button to size to content */
|
width: auto;
|
||||||
margin: 0; /* Remove default margins */
|
margin: 0;
|
||||||
white-space: nowrap; /* Prevent button text from wrapping */
|
white-space: nowrap;
|
||||||
|
color: white;
|
||||||
|
font-size: 12pt;
|
||||||
|
font-family: inter;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
max-width: none;
|
||||||
|
width: auto;
|
||||||
|
margin: 5px 0;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
.bluebutton, .greenbutton, .redbutton:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
#users {
|
||||||
|
display: flex;
|
||||||
|
align-items: left;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#meeting {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
align-items: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suttle {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#users {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#meeting {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.message.call-notification {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#meet {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
.eggs-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: -300px;
|
||||||
|
top: 0;
|
||||||
|
width: 300px;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eggs-panel.visible {
|
||||||
|
right: -310px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eggs-list {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.egg-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
font-family: "inter";
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.egg-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.egg-content {
|
||||||
|
padding: 10px;
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
#messaging {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messaging-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eggs-panel {
|
||||||
|
width: 300px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
transform: translateX(100%);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eggs-panel.visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eggs-list {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.egg-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
font-family: "inter";
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.egg-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.egg-content {
|
||||||
|
padding: 10px;
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-list {
|
||||||
|
width: 200px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-header {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-item {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-item.active {
|
||||||
|
background: rgba(104, 79, 255, 0.3);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-room-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
background: rgba(73, 255, 145, 0.3);
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-room-button:hover {
|
||||||
|
background: rgba(73, 255, 145, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.system-message {
|
||||||
|
color: rgba(73, 255, 145, 0.8);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.history-message {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Chookchat</title>
|
<title>Chookchat</title>
|
||||||
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
<link type="text/css" rel="stylesheet" href="index.css">
|
<link type="text/css" rel="stylesheet" href="index.css">
|
||||||
<link type="text/css" rel="stylesheet" href="gradient.css">
|
<link type="text/css" rel="stylesheet" href="gradient.css">
|
||||||
<link rel="shortcut icon" type="image/jpg" href="favicon.ico"/>
|
<link rel="shortcut icon" type="image/jpg" href="favicon.ico"/>
|
||||||
|
@ -31,14 +33,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden" id="messaging">
|
<div class="hidden" id="messaging">
|
||||||
|
<div class="messaging-container">
|
||||||
|
<div id="meet"></div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div id="messagebox" class="box" style="height: 600px;"><div></div></div>
|
<div id="users" class="suttle"></div>
|
||||||
|
<button id="meeting" class="bluebutton" onclick="startMeeting()">📞</button>
|
||||||
|
<div id="messagebox" class="box" style="height: 600px;">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div id="typing" class="suttle"></div>
|
||||||
|
<div id="upload" class="suttle" style="display: none;">
|
||||||
|
<input type="file" id="fileupload" accept="image/*,text/*">
|
||||||
|
<button class="bluebutton" onclick="uploadFile()">Upload</button>
|
||||||
|
</input>
|
||||||
|
</div>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="messageInput" placeholder="Send a message...">
|
<button onclick="showFileUpload()" class="bluebutton">📁</button>
|
||||||
|
<button onclick="showEggs()" class="bluebutton">🥚</button>
|
||||||
|
<input type="text" id="messageInput" placeholder="Send a message..." autofocus>
|
||||||
<button onclick="sendMessage()" class="bluebutton">Send</button>
|
<button onclick="sendMessage()" class="bluebutton">Send</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="eggs" style="display: none;">
|
||||||
|
<div id="eggs-list" class="eggs-list">
|
||||||
|
<button class="egg-item" onclick="eggNotepad()">📝 Notepad</button>
|
||||||
|
</div>
|
||||||
|
<!-- Eggs Start Here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,24 @@
|
||||||
alert("Chookchat is currently in early development, expect bugs! Please don't try breaking the public server, do that with your own test server (read more in the Git repo). Thanks for trying Chookchat!")
|
alert("Chookchat is currently in early development, expect bugs! Please don't try breaking the public server, do that with your own test server (read more in the Git repo). Thanks for trying Chookchat!")
|
||||||
|
|
||||||
const width = $(document).width();
|
|
||||||
|
|
||||||
if (width < 512) {
|
|
||||||
console.log("enlarging");
|
|
||||||
const loginDiv = document.getElementById('login');
|
|
||||||
if (loginDiv) {
|
|
||||||
console.log(loginDiv.style.width = `${width}px`);
|
|
||||||
console.log("enlarged?");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ws;
|
let ws;
|
||||||
let username;
|
let username;
|
||||||
let password;
|
let password;
|
||||||
|
let typingTimeout;
|
||||||
|
let typingPeople = new Array();
|
||||||
|
let api;
|
||||||
|
let currentRoom = "general";
|
||||||
|
let availableRooms = ["general"];
|
||||||
|
|
||||||
function resizeMessaging() {
|
function resizeMessaging() {
|
||||||
const messagingDiv = document.getElementById('messaging');
|
const messagingDiv = document.getElementById('messaging');
|
||||||
if (messagingDiv) {
|
if (messagingDiv) {
|
||||||
messagingDiv.style.width = `${window.innerWidth - 40}px`; // -40 for body margins
|
messagingDiv.style.width = `${window.innerWidth - 40}px`;
|
||||||
messagingDiv.style.height = `${window.innerHeight - 40}px`; // -40 for body margins
|
messagingDiv.style.height = `${window.innerHeight - 40}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call it initially
|
|
||||||
resizeMessaging();
|
resizeMessaging();
|
||||||
|
|
||||||
// Add resize listener to handle window resizing
|
|
||||||
window.addEventListener('resize', resizeMessaging);
|
window.addEventListener('resize', resizeMessaging);
|
||||||
|
|
||||||
function showConfig() {
|
function showConfig() {
|
||||||
|
@ -60,8 +52,82 @@ function getSignupUrl() {
|
||||||
|
|
||||||
return `${protocol}://${cleanUrl}:${serverPort}/api/createaccount/`;
|
return `${protocol}://${cleanUrl}:${serverPort}/api/createaccount/`;
|
||||||
}
|
}
|
||||||
|
function getUploadUrl() {
|
||||||
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||||||
|
const serverPort = document.getElementById('serverPort').value;
|
||||||
|
const useWss = document.getElementById('securityStatus').checked;
|
||||||
|
const protocol = useWss ? 'https' : 'http';
|
||||||
|
|
||||||
function connect() {
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
||||||
|
|
||||||
|
return `${protocol}://${cleanUrl}:${serverPort}/api/upload`;
|
||||||
|
}
|
||||||
|
async function getRooms() {
|
||||||
|
try {
|
||||||
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||||||
|
const serverPort = document.getElementById('serverPort').value;
|
||||||
|
const useWss = document.getElementById('securityStatus').checked;
|
||||||
|
const protocol = useWss ? 'https' : 'http';
|
||||||
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
||||||
|
const url = `${protocol}://${cleanUrl}:${serverPort}/api/rooms`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
mode: "no-cors"
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
return JSON.parse(data.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching rooms:', error);
|
||||||
|
return ["general"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoomHistory(roomName) {
|
||||||
|
try {
|
||||||
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||||||
|
const serverPort = document.getElementById('serverPort').value;
|
||||||
|
const useWss = document.getElementById('securityStatus').checked;
|
||||||
|
const protocol = useWss ? 'https' : 'http';
|
||||||
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
||||||
|
const url = `${protocol}://${cleanUrl}:${serverPort}/api/room/${roomName}/history`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
mode: "no-cors"
|
||||||
|
});
|
||||||
|
const history = await response.text();
|
||||||
|
return history;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching room history:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageMimeTypes = [
|
||||||
|
'image/webp',
|
||||||
|
'image/tiff',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/vnd.microsoft.icon',
|
||||||
|
'image/gif',
|
||||||
|
'image/bmp',
|
||||||
|
];
|
||||||
|
|
||||||
|
const imageTypes = [
|
||||||
|
'png',
|
||||||
|
'jpg',
|
||||||
|
'jpeg',
|
||||||
|
'svg',
|
||||||
|
'tiff',
|
||||||
|
'gif',
|
||||||
|
'webp',
|
||||||
|
'bmp'
|
||||||
|
];
|
||||||
|
|
||||||
|
function isImage(file) {
|
||||||
|
const fileSplit = file.split(".");
|
||||||
|
return imageTypes.includes(fileSplit[fileSplit.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
username = document.getElementById('username').value;
|
username = document.getElementById('username').value;
|
||||||
password = document.getElementById('password').value;
|
password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
@ -79,11 +145,22 @@ function connect() {
|
||||||
|
|
||||||
var incorrectDetail = 0;
|
var incorrectDetail = 0;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = async () => {
|
||||||
Notification.requestPermission();
|
if (typeof Notification !== "undefined") {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
console.log('Connected!');
|
console.log('Connected!');
|
||||||
document.getElementById('login').style.display = 'none';
|
document.getElementById('login').style.display = 'none';
|
||||||
document.getElementById('messaging').style.display = 'block';
|
document.getElementById('messaging').style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
availableRooms = await getRooms();
|
||||||
|
updateRoomList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get room list:', error);
|
||||||
|
availableRooms = ["general"];
|
||||||
|
}
|
||||||
|
|
||||||
const connectMessage = {
|
const connectMessage = {
|
||||||
"type": "connect",
|
"type": "connect",
|
||||||
"username": username,
|
"username": username,
|
||||||
|
@ -91,6 +168,9 @@ function connect() {
|
||||||
"content": `${username} joined the room!`
|
"content": `${username} joined the room!`
|
||||||
}
|
}
|
||||||
ws.send(JSON.stringify(connectMessage));
|
ws.send(JSON.stringify(connectMessage));
|
||||||
|
|
||||||
|
joinRoom("general");
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
if (event.data === "ping") {
|
if (event.data === "ping") {
|
||||||
ws.send("pong");
|
ws.send("pong");
|
||||||
|
@ -109,20 +189,168 @@ function connect() {
|
||||||
incorrectDetail = 1;
|
incorrectDetail = 1;
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
if (message.content == "banned") {
|
||||||
|
alert("kiddo you're banned lol what did you do to get banned lmaooo");
|
||||||
|
incorrectDetail = 1;
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const messagesDiv = document.getElementById('messagebox');
|
else if (message.type == "typing" && message.content == "1") {
|
||||||
const messageElement = document.createElement('div');
|
if (username !== message.username && !typingPeople.includes(message.username)) {
|
||||||
if (messageElement) {
|
typingPeople.push(message.username);
|
||||||
if (messagesDiv) {
|
updatePeopleTyping();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "typing" && message.content == "0") {
|
||||||
|
if (username !== message.username && typingPeople.includes(message.username)) {
|
||||||
|
const index = typingPeople.indexOf(message.username);
|
||||||
|
typingPeople.splice(index, 1);
|
||||||
|
updatePeopleTyping();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "users" && message.username == "system") {
|
||||||
|
usersDiv = document.getElementById("users");
|
||||||
|
if (usersDiv) {
|
||||||
|
usersDiv.textContent = `Online users: ${message.content}`
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "roomUsers" && message.username == "system") {
|
||||||
|
const usersInRoom = message.content;
|
||||||
|
const roomName = message.room;
|
||||||
|
usersDiv = document.getElementById("users");
|
||||||
|
if (usersDiv) {
|
||||||
|
usersDiv.textContent = `Users in ${roomName}: ${usersInRoom}`;
|
||||||
|
if (roomName !== currentRoom) {
|
||||||
|
currentRoom = roomName;
|
||||||
|
updateCurrentRoomDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "roomsList" && message.username == "system") {
|
||||||
|
try {
|
||||||
|
availableRooms = JSON.parse(message.content);
|
||||||
|
updateRoomList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing rooms list:', error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "roomCreated" || message.type == "roomJoin") {
|
||||||
|
if (message.room) {
|
||||||
|
currentRoom = message.room;
|
||||||
|
updateCurrentRoomDisplay();
|
||||||
|
clearMessages();
|
||||||
|
loadRoomHistory(currentRoom);
|
||||||
|
updateRoomList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the system message
|
||||||
|
const messagesDiv = document.getElementById('messagebox');
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
if (messageElement && messagesDiv) {
|
||||||
messagesDiv.appendChild(messageElement);
|
messagesDiv.appendChild(messageElement);
|
||||||
|
messageElement.className = 'message system-message';
|
||||||
|
messageElement.textContent = message.content;
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
messageElement.className = 'message';
|
}
|
||||||
messageElement.textContent = `${message.username}: ${message.content}` ;
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "file") {
|
||||||
|
// Only show file if it's for current room
|
||||||
|
if (message.room && message.room !== currentRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messagesDiv = document.getElementById('messagebox');
|
||||||
|
let filename = message.content.replace("https://maxwellj.xyz/chookchat/uploads/", "");
|
||||||
|
if (isImage(filename)) {
|
||||||
|
const imagePreview = document.createElement('img');
|
||||||
|
if (imagePreview) {
|
||||||
|
if (messagesDiv) {
|
||||||
|
messagesDiv.appendChild(imagePreview);
|
||||||
|
imagePreview.src = message.content;
|
||||||
|
imagePreview.height = 300;
|
||||||
|
imagePreview.addEventListener("click", function() {
|
||||||
|
window.open(message.content, "_blank");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fileButton = document.createElement('button');
|
||||||
|
if (fileButton) {
|
||||||
|
if (messagesDiv) {
|
||||||
|
messagesDiv.appendChild(fileButton);
|
||||||
|
fileButton.textContent = `Open ${filename} in new tab`;
|
||||||
|
fileButton.className = "bluebutton";
|
||||||
|
fileButton.addEventListener("click", function() {
|
||||||
|
window.open(message.content, "_blank");
|
||||||
|
});
|
||||||
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (message.type == "call") {
|
||||||
|
if (message.room && message.room !== currentRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesDiv = document.getElementById('messagebox');
|
||||||
|
const callButton = document.createElement('div');
|
||||||
|
if (callButton) {
|
||||||
|
if (messagesDiv) {
|
||||||
|
messagesDiv.appendChild(callButton)
|
||||||
|
callButton.className = "message call-notification";
|
||||||
|
callButton.textContent = `${message.username} started a Jitsi call! Click to join!`;
|
||||||
|
callButton.addEventListener('click', () => {
|
||||||
|
window.open(message.content, '_blank');
|
||||||
|
});
|
||||||
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (document.hidden) {
|
else if (message.type == "connect") {
|
||||||
const notifiction = new Notification("Chookchat", {body: messageElement.textContent});
|
const messagesDiv = document.getElementById('messagebox');
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
if (messageElement) {
|
||||||
|
if (messagesDiv) {
|
||||||
|
messagesDiv.appendChild(messageElement);
|
||||||
|
messageElement.className = 'message';
|
||||||
|
messageElement.textContent = message.content;
|
||||||
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (document.hidden) {
|
||||||
|
const notifiction = new Notification("Chookchat", {body: messageElement.textContent});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.type == "message") {
|
||||||
|
if (message.room && message.room !== currentRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesDiv = document.getElementById('messagebox');
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
if (messageElement) {
|
||||||
|
if (messagesDiv) {
|
||||||
|
messagesDiv.appendChild(messageElement);
|
||||||
|
messageElement.className = 'message';
|
||||||
|
messageElement.textContent = `${message.username}: ${message.content}` ;
|
||||||
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (document.hidden) {
|
||||||
|
const notifiction = new Notification("Chookchat", {body: messageElement.textContent});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -130,7 +358,40 @@ function connect() {
|
||||||
alert("Chookchat has disconnected :/ Refresh the page to try again");
|
alert("Chookchat has disconnected :/ Refresh the page to try again");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function createRoom(roomName) {
|
||||||
|
const message = {
|
||||||
|
type: 'createRoom',
|
||||||
|
username: username,
|
||||||
|
token: md5(password),
|
||||||
|
room: roomName,
|
||||||
|
content: ""
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
setTimeout(updateRoomList, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinRoom(roomName) {
|
||||||
|
if (roomName === currentRoom) {
|
||||||
|
const usersMessage = {
|
||||||
|
type: 'getUsersInRoom',
|
||||||
|
username: username,
|
||||||
|
token: md5(password),
|
||||||
|
room: roomName,
|
||||||
|
content: ""
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(usersMessage));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
type: 'joinRoom',
|
||||||
|
username: username,
|
||||||
|
token: md5(password),
|
||||||
|
room: roomName,
|
||||||
|
content: ""
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const message = messageInput.value.trim();
|
const message = messageInput.value.trim();
|
||||||
|
@ -139,12 +400,74 @@ function sendMessage() {
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"username": username,
|
"username": username,
|
||||||
"token": md5(password),
|
"token": md5(password),
|
||||||
|
"room": currentRoom,
|
||||||
"content": message
|
"content": message
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedMessage && ws && ws.readyState === WebSocket.OPEN) {
|
if (processedMessage.content && ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify(processedMessage));
|
ws.send(JSON.stringify(processedMessage));
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
|
|
||||||
|
if (typingTimeout) {
|
||||||
|
clearTimeout(typingTimeout);
|
||||||
|
}
|
||||||
|
const stoppedTypingMessage = {
|
||||||
|
"type": "typing",
|
||||||
|
"username": username,
|
||||||
|
"token": md5(password),
|
||||||
|
"room": currentRoom,
|
||||||
|
"content": "0"
|
||||||
|
};
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(stoppedTypingMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFileUpload() {
|
||||||
|
const fileUploadElement = document.getElementById("upload");
|
||||||
|
if (fileUploadElement) {
|
||||||
|
fileUploadElement.style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile() {
|
||||||
|
const fileInput = document.getElementById("fileupload");
|
||||||
|
if (!fileInput.files.length) {
|
||||||
|
alert("Please add a file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", fileInput.files[0]);
|
||||||
|
formData.append("room", currentRoom);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getUploadUrl(), {
|
||||||
|
method: 'POST',
|
||||||
|
mode: "no-cors",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.text();
|
||||||
|
const processedMessage = {
|
||||||
|
"type": "message",
|
||||||
|
"username": username,
|
||||||
|
"token": md5(password),
|
||||||
|
"room": currentRoom,
|
||||||
|
"content": `Sent a file`
|
||||||
|
}
|
||||||
|
if (processedMessage && ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(processedMessage));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Something went wrong lmao");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(error);
|
||||||
|
}
|
||||||
|
const fileUploadElement = document.getElementById("upload");
|
||||||
|
if (fileUploadElement) {
|
||||||
|
fileUploadElement.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +483,11 @@ document.getElementById('password').addEventListener('keypress', (event) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function register() {
|
async function doRegister(username, password) {
|
||||||
|
return fetch(`${getSignupUrl()}username:{${username}}token:{${md5(password)}}`).then((response)=>response.json()).then((responseJson)=>{return responseJson});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register() {
|
||||||
username = document.getElementById('username').value;
|
username = document.getElementById('username').value;
|
||||||
password = document.getElementById('password').value;
|
password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
@ -169,5 +496,206 @@ function register() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(`${getSignupUrl()}username:{${username}}token:{${md5(password)}}`);
|
const response = await this.doRegister(username, password);
|
||||||
|
if (response.type == "success") {
|
||||||
|
alert("Account created! Click 'log in' to access Chookchat!")
|
||||||
|
} else {
|
||||||
|
alert(`We couldn't create your account :( Reason: ${response.content}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTypingIndicator() {
|
||||||
|
if (typingTimeout) {
|
||||||
|
clearTimeout(typingTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typingMessage = {
|
||||||
|
"type": "typing",
|
||||||
|
"username": username,
|
||||||
|
"token": md5(password),
|
||||||
|
"room": currentRoom,
|
||||||
|
"content": "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(typingMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
typingTimeout = setTimeout(() => {
|
||||||
|
const stoppedTypingMessage = {
|
||||||
|
"type": "typing",
|
||||||
|
"username": username,
|
||||||
|
"token": md5(password),
|
||||||
|
"room": currentRoom,
|
||||||
|
"content": "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(stoppedTypingMessage));
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePeopleTyping() {
|
||||||
|
const typingDiv = document.getElementById('typing');
|
||||||
|
if (typingDiv) {
|
||||||
|
if (typingPeople.length === 0) {
|
||||||
|
typingDiv.textContent = '';
|
||||||
|
} else if (typingPeople.length === 1) {
|
||||||
|
typingDiv.textContent = `${typingPeople[0]} is typing...`;
|
||||||
|
} else if (typingPeople.length === 2) {
|
||||||
|
typingDiv.textContent = `${typingPeople[0]} and ${typingPeople[1]} are typing...`;
|
||||||
|
} else {
|
||||||
|
typingDiv.textContent = `${typingPeople.length} people are typing...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const characters ='abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
function generateString(length) {
|
||||||
|
let result = '';
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
for ( let i = 0; i < length; i++ ) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMeeting() {
|
||||||
|
const link = `https://meet.jit.si/chookchat-${generateString(15)}`;
|
||||||
|
alert("Note: You may need to sign in to Jitsi to start the meeting. We'll take you there in a moment...")
|
||||||
|
window.open(link, '_blank');
|
||||||
|
const processedMessage = {
|
||||||
|
"type": "call",
|
||||||
|
"username": username,
|
||||||
|
"token": md5(password),
|
||||||
|
"room": currentRoom,
|
||||||
|
"content": link
|
||||||
|
};
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(processedMessage));
|
||||||
|
} else {
|
||||||
|
alert("Something went wrong. Refreshing might do the trick :)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('messageInput').addEventListener('input', startTypingIndicator);
|
||||||
|
|
||||||
|
async function updateRoomList() {
|
||||||
|
let roomListDiv = document.getElementById('room-list');
|
||||||
|
if (!roomListDiv) {
|
||||||
|
const messagingDiv = document.querySelector('.messaging-container');
|
||||||
|
roomListDiv = document.createElement('div');
|
||||||
|
roomListDiv.id = 'room-list';
|
||||||
|
roomListDiv.className = 'room-list';
|
||||||
|
messagingDiv.insertBefore(roomListDiv, messagingDiv.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
availableRooms = await getRooms();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating room list:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
roomListDiv.innerHTML = '<div class="room-header">Rooms</div>';
|
||||||
|
|
||||||
|
availableRooms.forEach(room => {
|
||||||
|
const roomElement = document.createElement('div');
|
||||||
|
roomElement.className = `room-item ${room === currentRoom ? 'active' : ''}`;
|
||||||
|
roomElement.textContent = room;
|
||||||
|
roomElement.addEventListener('click', () => joinRoom(room));
|
||||||
|
roomListDiv.appendChild(roomElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRoomButton = document.createElement('button');
|
||||||
|
createRoomButton.className = 'create-room-button';
|
||||||
|
createRoomButton.textContent = '+ New Room';
|
||||||
|
createRoomButton.addEventListener('click', promptCreateRoom);
|
||||||
|
roomListDiv.appendChild(createRoomButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentRoomDisplay() {
|
||||||
|
const roomTitle = document.getElementById('room-title');
|
||||||
|
if (roomTitle) {
|
||||||
|
roomTitle.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomItems = document.querySelectorAll('.room-item');
|
||||||
|
roomItems.forEach(item => {
|
||||||
|
if (item.textContent === currentRoom) {
|
||||||
|
item.classList.add('active');
|
||||||
|
} else {
|
||||||
|
item.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMessages() {
|
||||||
|
const messagebox = document.getElementById('messagebox');
|
||||||
|
if (messagebox) {
|
||||||
|
messagebox.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRoomHistory(roomName) {
|
||||||
|
try {
|
||||||
|
const history = await getRoomHistory(roomName);
|
||||||
|
if (history) {
|
||||||
|
const messagebox = document.getElementById('messagebox');
|
||||||
|
const lines = history.split('\n');
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (line.trim()) {
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.className = 'message history-message';
|
||||||
|
messageElement.textContent = line;
|
||||||
|
messagebox.appendChild(messageElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
messagebox.scrollTop = messagebox.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading room history:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptCreateRoom() {
|
||||||
|
const roomName = prompt('Enter a name for the new room:');
|
||||||
|
if (roomName && roomName.trim()) {
|
||||||
|
createRoom(roomName.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEggs() {
|
||||||
|
const eggsPanel = document.getElementById('eggs');
|
||||||
|
eggsPanel.style.display = "block";
|
||||||
|
const mainBox = document.querySelector('#messaging .box');
|
||||||
|
if (eggsPanel.classList.contains('visible')) {
|
||||||
|
mainBox.style.width = 'calc(100% - 310px)';
|
||||||
|
} else {
|
||||||
|
mainBox.style.width = '100%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEggs() {
|
||||||
|
const eggsPanel = document.getElementById('eggs');
|
||||||
|
eggsPanel.style.display = "block";
|
||||||
|
}
|
||||||
|
const uploadField = document.getElementById("fileupload");
|
||||||
|
|
||||||
|
uploadField.onchange = function() {
|
||||||
|
if(this.files[0].size > 10485760) {
|
||||||
|
alert("That file is too big bro. Not as big as my (message terminated)");
|
||||||
|
this.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eggs begin here
|
||||||
|
|
||||||
|
|
||||||
|
|
152
server/README.md
152
server/README.md
|
@ -1,130 +1,62 @@
|
||||||
# Chookpen Server
|
# Chookchat Server
|
||||||
|
|
||||||
Key features:
|
This is the code for Chookchat's server program. The server program:
|
||||||
- Real-time messaging with WebSocket support
|
|
||||||
- User authentication and account management
|
|
||||||
- Persistent message history
|
|
||||||
- Simple HTTP API for basic operations
|
|
||||||
- Built-in static file serving for web clients
|
|
||||||
- Support for encryption keys
|
|
||||||
- Active user tracking and presence notifications
|
|
||||||
|
|
||||||
## Requirements
|
* Starts a server on port 7070
|
||||||
|
|
||||||
- Java 17 or later
|
* Hosts the web client files (index.html at the root)
|
||||||
- Caddy (for HTTPS reverse proxy) - [Download from caddyserver.com](https://caddyserver.com/download)
|
|
||||||
- Some kind of OS, any should do
|
|
||||||
- 50mb free space, 30mb avaliable ram (yes I know, very bloated and inefficient)
|
|
||||||
|
|
||||||
## Running the Server
|
* Adjusts index.html based on your config file (a backup index.html is still avaliable at localhost:7070/index.html)
|
||||||
|
|
||||||
Prebuilt binaries are available for immediate use. Simply download the latest server release and run:
|
* Handles user signups
|
||||||
|
|
||||||
```bash
|
* Opens a websocket allowing for flexible user input
|
||||||
./run
|
|
||||||
|
* Handles file uploads
|
||||||
|
|
||||||
|
**Note:** Chookchat does not support HTTPS natively. We recommend the usage of a reverse proxy like Caddy (caddyserver.com) to enable HTTPS on your website, as well as a firewall to block HTTP connections.
|
||||||
|
|
||||||
|
## Server Configuration
|
||||||
|
|
||||||
|
Chookchat looks for a chookchat.config file in the directory where you start Chookchat. An example is provided in this directory. Your file must look like the following:
|
||||||
|
```
|
||||||
|
address:localhost;port:7070;security:false;serviceName:Chookchat;
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will start on port 7070 by default.
|
Address: Where your server is hosted (a domain or IP address usually)
|
||||||
|
Port: Your server's port (default 7070, 443 if you route it through Caddy with HTTPS)
|
||||||
## Building from Source
|
Security: Whether or not to use WSS/HTTPS (either `true` or `false`)
|
||||||
|
ServiceName: What your server's name comes up as.
|
||||||
First, install Gradle. Unless you want to painfully compile everything manually, Gradle is your best friend.
|
|
||||||
|
|
||||||
Clone the repository and CD into the server directory. Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gradle build
|
|
||||||
```
|
|
||||||
|
|
||||||
To create a distribution (you don't need to run gradle build, it will do that for you):
|
|
||||||
```bash
|
|
||||||
./gradlew installDist
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the server, create the files `chatHistory` and `userDatabase` in the directory you're running it from, then run the script to start Chookpen.
|
|
||||||
|
|
||||||
## API Documentation
|
## API Documentation
|
||||||
|
|
||||||
### HTTP Endpoints
|
Chookchat uses websockets to send and recieve messages. Each websocket request (except pinging and ponging) contains JSON to show what kind of data we are sending, what user is sending it (and token while being sent to the server), and the actual message content. A chookchat websocket message to the server looks like this:
|
||||||
|
```json
|
||||||
#### Send Message
|
{
|
||||||
- Endpoint: `/api/send/{content}`
|
"type": "message",
|
||||||
- Content Format: `username:{name}token:{token}message:{message}`
|
"username": "max",
|
||||||
- Response: "Success" or error message
|
"token": "(hash of password)",
|
||||||
|
"content": "dongus"
|
||||||
#### Create Account
|
|
||||||
- Endpoint: `/api/createaccount/{content}`
|
|
||||||
- Content Format: `username:{name}token:{password}`
|
|
||||||
- Response: "Success" or error message
|
|
||||||
|
|
||||||
#### Sync Messages
|
|
||||||
- Endpoint: `/api/syncmessages/{content}`
|
|
||||||
- Content Format: `username:{name}token:{token}`
|
|
||||||
- Response: Chat history or error message
|
|
||||||
|
|
||||||
#### Auth Key (being worked on for E2EE)
|
|
||||||
- Endpoint: `/api/authkey/{content}`
|
|
||||||
- Content Format: `username:{name}token:{token}authkey:{key}`
|
|
||||||
- Response: "Success" or error message
|
|
||||||
|
|
||||||
## WebSocket Interface
|
|
||||||
|
|
||||||
Connect to `/api/websocket` for real-time updates.
|
|
||||||
|
|
||||||
### WebSocket Messages
|
|
||||||
|
|
||||||
1. Server to Client:
|
|
||||||
- Ping messages: "ping"
|
|
||||||
- User updates: "!users:{user1,user2,user3}"
|
|
||||||
- Chat messages: "username: message"
|
|
||||||
|
|
||||||
2. Client to Server:
|
|
||||||
- Pong response: "pong"
|
|
||||||
- Message format: Same as HTTP send endpoint
|
|
||||||
|
|
||||||
## Setting up HTTPS with Caddy
|
|
||||||
|
|
||||||
Caddy provides automatic HTTPS and serves as a reverse proxy for your Chookpen server. [Download from caddyserver.com](https://caddyserver.com/download) or from your Linux/BSD/Illumos/Haiku/TempleOS/whatever distribution's package manager.
|
|
||||||
|
|
||||||
1. Create a `Caddyfile` in your server directory:
|
|
||||||
```
|
|
||||||
chat.yourdomain.com {
|
|
||||||
reverse_proxy localhost:7070
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
And a message from the server would look like this (it's the same without the token):
|
||||||
2. Start Caddy:
|
```json
|
||||||
```bash
|
{
|
||||||
caddy run
|
"type": "message",
|
||||||
|
"username": "max",
|
||||||
|
"content": "dongus"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
The server will send a `ping` every five seconds (without JSON) to stop the connection from timing out. When you recieve this, send back a `pong`.
|
||||||
|
|
||||||
Caddy will automatically obtain and manage SSL certificates for your domain.
|
### Message Types
|
||||||
|
|
||||||
## Client Deployment
|
**message**: A message. Not much going on. It's recommended to display it like ${username}: ${content}
|
||||||
|
|
||||||
Place your client files in the `src/main/resources/public` directory. The server will automatically serve these static files, making the client accessible at your server's root URL.
|
**call**: A link to a Jitsi call. Treat this how you'd like.
|
||||||
|
|
||||||
## Maintenance
|
**file**: A file sent by a user. For now, it appears as if the `system` user has sent the file, so in a client send a message describing who's uploaded the file shortly after sending the file.
|
||||||
|
|
||||||
### Database Files
|
**connect**: A user joining the room. If this is not sent by the `system` user, ignore.
|
||||||
- `userDatabase`: Contains user credentials in format `username:token:salt`
|
|
||||||
- `chatHistory`: Stores message history
|
|
||||||
- Regular backups recommended
|
|
||||||
|
|
||||||
### Health Checks
|
**users**: A list of users currently in the room, seperated by ", ". In order of who joined first. If this is not sent by the `system` user, ignore.
|
||||||
- Server automatically maintains WebSocket connections
|
|
||||||
- Dead sessions are cleaned up automatically
|
|
||||||
- Active user count is logged
|
|
||||||
|
|
||||||
### Security Recommendations
|
|
||||||
- Keep database files secure
|
|
||||||
- Regular system updates
|
|
||||||
- Monitor for unusual login patterns
|
|
||||||
- Back up regularly:
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
backup_dir="/path/to/backups"
|
|
||||||
date_stamp=$(date +%Y%m%d)
|
|
||||||
cp userDatabase "${backup_dir}/userDatabase_${date_stamp}"
|
|
||||||
cp chatHistory "${backup_dir}/chatHistory_${date_stamp}"
|
|
||||||
```
|
|
||||||
|
|
1
server/chookchat.config
Normal file
1
server/chookchat.config
Normal file
|
@ -0,0 +1 @@
|
||||||
|
address:localhost;port:7070;security:false;serviceName:chookchat;
|
1
server/chookchat.eggs.config
Normal file
1
server/chookchat.eggs.config
Normal file
|
@ -0,0 +1 @@
|
||||||
|
notepad
|
4
server/eggs/notepad/index.html
Normal file
4
server/eggs/notepad/index.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<div class="egg" id="egg-notepad" style="display: none;">
|
||||||
|
<button id="egg-notepad-close" class="redbutton" onclick="closeEggNotepad()">Close</button><br>
|
||||||
|
<textarea id="egg-notepad-textarea" placeholder="Start typing..." style="height: 500px"></textarea>
|
||||||
|
</div>
|
21
server/eggs/notepad/index.js
Normal file
21
server/eggs/notepad/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
function eggNotepad() {
|
||||||
|
const eggsList = document.getElementById("eggs-list");
|
||||||
|
eggsList.style.display = "none";
|
||||||
|
const eggNotepad = document.getElementById("egg-notepad");
|
||||||
|
eggNotepad.style.display = "block";
|
||||||
|
const eggNotepadTextArea = document.getElementById("egg-notepad-textarea");
|
||||||
|
eggNotepadTextArea.addEventListener('input', function(event) {
|
||||||
|
const eggNotepadMessage = {
|
||||||
|
"type": "egg-notepad",
|
||||||
|
"username": username,
|
||||||
|
"token": md5(password),
|
||||||
|
"content": event.target.value
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(eggNotepadMessage));
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEggNotepad() {
|
||||||
|
const eggNotepad = document.getElementById("egg-notepad");
|
||||||
|
eggNotepad.style.display = "none";
|
||||||
|
}
|
4
server/eggs/notepad/message.js
Normal file
4
server/eggs/notepad/message.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
else if (message.type == "egg-notepad") {
|
||||||
|
const eggNotepadTextArea = document.getElementById("egg-notepad-textarea");
|
||||||
|
eggNotepadTextArea.value = message.content
|
||||||
|
}
|
1
server/resources/InterVariable.ttf
Symbolic link
1
server/resources/InterVariable.ttf
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../client-web/InterVariable.ttf
|
1
server/resources/gradient.css
Symbolic link
1
server/resources/gradient.css
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../client-web/gradient.css
|
1
server/resources/index.css
Symbolic link
1
server/resources/index.css
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../client-web/index.css
|
1
server/resources/index.html
Symbolic link
1
server/resources/index.html
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../client-web/index.html
|
1
server/resources/index.js
Symbolic link
1
server/resources/index.js
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../client-web/index.js
|
|
@ -1,5 +1,5 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||||
}
|
}
|
||||||
rootProject.name = "chookpen"
|
rootProject.name = "chookchat"
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ package xyz.maxwellj.chookchat
|
||||||
|
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import io.javalin.websocket.WsContext
|
import io.javalin.websocket.WsContext
|
||||||
|
import io.javalin.http.UploadedFile
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@ -24,32 +26,194 @@ fun md5(input:String): String {
|
||||||
val md = MessageDigest.getInstance("MD5")
|
val md = MessageDigest.getInstance("MD5")
|
||||||
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
|
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
fun removeLines(fileName: String, lineNumber: String) {
|
object config {
|
||||||
require(!fileName.isEmpty() && startLine >= 1 && numLines >= 1)
|
var address = ""
|
||||||
val f = File(fileName)
|
var port = ""
|
||||||
var lines = f.readLines()
|
var security = ""
|
||||||
if (startLine > size) {
|
var serviceName = ""
|
||||||
println("The starting line is beyond the length of the file")
|
|
||||||
return
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines = lines.take(startLine - 1) + lines.drop(startLine + n - 1)
|
|
||||||
val text = lines.joinToString(System.lineSeparator())
|
|
||||||
f.writeText(text)
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
object WsSessionManager {
|
object WsSessionManager {
|
||||||
val peopleOnline = mutableListOf("")
|
val peopleOnline = mutableListOf<String>()
|
||||||
val sessionsList = mutableListOf("")
|
val sessionsList = mutableListOf<String>()
|
||||||
val sessions = ConcurrentHashMap<WsContext, String>()
|
val sessions = ConcurrentHashMap<WsContext, String>()
|
||||||
val sessionIds = ConcurrentHashMap<String, WsContext>()
|
val sessionIds = ConcurrentHashMap<String, WsContext>()
|
||||||
val userSessions = ConcurrentHashMap<String, String>()
|
val userSessions = ConcurrentHashMap<String, String>()
|
||||||
|
|
||||||
|
val roomList = mutableListOf<String>()
|
||||||
|
val userRooms = ConcurrentHashMap<String, String>()
|
||||||
|
val roomUsers = ConcurrentHashMap<String, MutableList<String>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
createRoom("general")
|
||||||
|
|
||||||
fixedRateTimer("websocket-ping", period = 5000) {
|
fixedRateTimer("websocket-ping", period = 5000) {
|
||||||
sendPing()
|
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() {
|
private fun sendPing() {
|
||||||
val deadSessions = mutableListOf<WsContext>()
|
val deadSessions = mutableListOf<WsContext>()
|
||||||
|
|
||||||
|
@ -65,31 +229,13 @@ object WsSessionManager {
|
||||||
deadSessions.add(ctx)
|
deadSessions.add(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any dead sessions
|
|
||||||
deadSessions.forEach { removeSession(it) }
|
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) {
|
fun addSession(ctx: WsContext) {
|
||||||
try {
|
try {
|
||||||
val sessionId = UUID.randomUUID().toString()
|
val sessionId = UUID.randomUUID().toString()
|
||||||
sessionsList.add(sessionId) // Changed from += to add()
|
sessionsList.add(sessionId)
|
||||||
sessions[ctx] = sessionId
|
sessions[ctx] = sessionId
|
||||||
sessionIds[sessionId] = ctx
|
sessionIds[sessionId] = ctx
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -101,10 +247,18 @@ object WsSessionManager {
|
||||||
try {
|
try {
|
||||||
val sessionId = sessions[ctx]
|
val sessionId = sessions[ctx]
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
// Find and remove the username associated with this session
|
|
||||||
userSessions.entries.find { it.value == sessionId }?.let { entry ->
|
userSessions.entries.find { it.value == sessionId }?.let { entry ->
|
||||||
peopleOnline.remove(entry.key)
|
val username = entry.key
|
||||||
userSessions.remove(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)
|
sessionsList.remove(sessionId)
|
||||||
|
@ -140,48 +294,202 @@ object WsSessionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any dead sessions
|
|
||||||
deadSessions.forEach { removeSession(it) }
|
deadSessions.forEach { removeSession(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSessionCount(): Int = sessions.size
|
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 {
|
fun extractMessageContent(inputData: String, ctx: WsContext): String {
|
||||||
val jsonInputData = JSONObject(inputData)
|
val jsonInputData = JSONObject(inputData)
|
||||||
|
|
||||||
if (jsonInputData.getString("type") == "connect") {
|
if (jsonInputData.getString("type") == "connect") {
|
||||||
val username = jsonInputData.getString("username")
|
val username = jsonInputData.getString("username")
|
||||||
WsSessionManager.associateUserWithSession(username, ctx)
|
WsSessionManager.associateUserWithSession(username, ctx)
|
||||||
WsSessionManager.handleUserLogin(username)
|
WsSessionManager.handleUserLogin(username)
|
||||||
|
|
||||||
val processedData = JSONObject().apply {
|
val processedData = JSONObject().apply {
|
||||||
put("type", "connect")
|
put("type", "connect")
|
||||||
put("username", "system")
|
put("username", "system")
|
||||||
put("content", "${jsonInputData.getString("username")} just joined the room!")
|
put("content", "${jsonInputData.getString("username")} just joined the chat!")
|
||||||
}
|
}
|
||||||
return(processedData.toString())
|
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 {
|
val processedData = JSONObject().apply {
|
||||||
put("type", jsonInputData.getString("type"))
|
put("type", jsonInputData.getString("type"))
|
||||||
put("username", jsonInputData.getString("username"))
|
put("username", username)
|
||||||
|
put("room", room)
|
||||||
put("content", jsonInputData.getString("content"))
|
put("content", jsonInputData.getString("content"))
|
||||||
}
|
}
|
||||||
return(processedData.toString())
|
|
||||||
|
return processedData.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleSentMessage(inputData: String): String {
|
fun handleSentMessage(inputData: String): String {
|
||||||
println("API request recieved: $inputData")
|
println("API request received: $inputData")
|
||||||
var jsonInputData: JSONObject
|
var jsonInputData: JSONObject
|
||||||
try {jsonInputData = JSONObject(inputData)} catch (error: JSONException){return(error.toString())}
|
try {
|
||||||
|
jsonInputData = JSONObject(inputData)
|
||||||
|
} catch (error: JSONException) {
|
||||||
|
return error.toString()
|
||||||
|
}
|
||||||
|
|
||||||
val username = jsonInputData.getString("username")
|
val username = jsonInputData.getString("username")
|
||||||
val token = jsonInputData.getString("token")
|
val token = jsonInputData.getString("token")
|
||||||
val content = jsonInputData.getString("content")
|
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())
|
val userDatabaseParser = BufferedReader(File("userDatabase").reader())
|
||||||
var lineNumber = 1
|
var lineNumber = 1
|
||||||
var userLine = ""
|
var userLine = ""
|
||||||
|
|
||||||
// Search the user database to find required information about the user
|
|
||||||
userDatabaseParser.forEachLine { line ->
|
userDatabaseParser.forEachLine { line ->
|
||||||
if (line.contains(username)) {
|
if (line.contains(username)) {
|
||||||
userLine = line
|
userLine = line
|
||||||
|
@ -196,7 +504,7 @@ fun handleSentMessage(inputData: String): String {
|
||||||
put("username", "system")
|
put("username", "system")
|
||||||
put("content", "unknown-account")
|
put("content", "unknown-account")
|
||||||
}
|
}
|
||||||
return(processedData.toString())
|
return processedData.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
var usernameInDatabase = ""
|
var usernameInDatabase = ""
|
||||||
|
@ -221,44 +529,147 @@ fun handleSentMessage(inputData: String): String {
|
||||||
tokenInDatabase = tokenInDatabase.replace(":", "")
|
tokenInDatabase = tokenInDatabase.replace(":", "")
|
||||||
saltInDatabase = saltInDatabase.replace(":", "")
|
saltInDatabase = saltInDatabase.replace(":", "")
|
||||||
banStatus = banStatus.replace(":", "")
|
banStatus = banStatus.replace(":", "")
|
||||||
|
|
||||||
if (banStatus == "1") {
|
if (banStatus == "1") {
|
||||||
val processedData = JSONObject().apply {
|
val processedData = JSONObject().apply {
|
||||||
put("type", "error")
|
put("type", "error")
|
||||||
put("username", "system")
|
put("username", "system")
|
||||||
put("content", "banned")
|
put("content", "banned")
|
||||||
}
|
}
|
||||||
return(processedData.toString())
|
return processedData.toString()
|
||||||
}
|
}
|
||||||
val tokenWithSalt = (md5(token + saltInDatabase))
|
|
||||||
/*println(saltInDatabase)
|
|
||||||
println(tokenWithSalt)
|
|
||||||
if (tokenWithSalt != tokenInDatabase) {*/
|
|
||||||
if (token != tokenInDatabase) {
|
if (token != tokenInDatabase) {
|
||||||
val processedData = JSONObject().apply {
|
val processedData = JSONObject().apply {
|
||||||
put("type", "error")
|
put("type", "error")
|
||||||
put("username", "system")
|
put("username", "system")
|
||||||
put("content", "invalid-token")
|
put("content", "invalid-token")
|
||||||
}
|
}
|
||||||
return(processedData.toString())
|
return processedData.toString()
|
||||||
}
|
}
|
||||||
// Make the message to respond to the client
|
|
||||||
val chatHistoryView = File("chatHistory")
|
|
||||||
var fullMessage = ""
|
|
||||||
if (content != "") {
|
if (content != "") {
|
||||||
fullMessage = "${chatHistoryView.readText()}$username: $content"
|
if (type != "message") {
|
||||||
// Add the client's message to the chat history
|
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")
|
val chatHistory = File("chatHistory")
|
||||||
chatHistory.appendText("$username: $content ${System.lineSeparator()}")
|
chatHistory.appendText("$username: $content [Room: $room] ${System.lineSeparator()}")
|
||||||
return("Success")
|
|
||||||
|
return "Success"
|
||||||
} else {
|
} else {
|
||||||
return("No data provided")
|
return "No data provided"
|
||||||
}
|
}
|
||||||
return("Chookchat")
|
}
|
||||||
|
|
||||||
|
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 {
|
fun createAccount(inputData: String): String {
|
||||||
println("Account creation request recieved: $inputData")
|
println("Account creation request recieved: $inputData")
|
||||||
// Parse data sent to the server by client
|
|
||||||
var username = ""
|
var username = ""
|
||||||
var token = ""
|
var token = ""
|
||||||
var message = ""
|
var message = ""
|
||||||
|
@ -289,7 +700,6 @@ fun createAccount(inputData: String): String {
|
||||||
var lineNumber = 1
|
var lineNumber = 1
|
||||||
var userExists = 0
|
var userExists = 0
|
||||||
|
|
||||||
// Search the user database to find required information about the user
|
|
||||||
var response = ""
|
var response = ""
|
||||||
userDatabaseParser.forEachLine { line ->
|
userDatabaseParser.forEachLine { line ->
|
||||||
if (line.contains(username)) {
|
if (line.contains(username)) {
|
||||||
|
@ -334,31 +744,81 @@ fun createAccount(inputData: String): String {
|
||||||
return(processedData.toString())
|
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>) {
|
fun main(args: Array<String>) {
|
||||||
WsSessionManager.peopleOnline.removeAt(0)
|
WsSessionManager.peopleOnline.clear()
|
||||||
WsSessionManager.sessionsList.removeAt(0)
|
WsSessionManager.sessionsList.clear()
|
||||||
|
WsSessionManager.roomList.clear()
|
||||||
|
|
||||||
|
WsSessionManager.createRoom("general")
|
||||||
|
|
||||||
|
val roomDirectory = File("roomChats")
|
||||||
|
if (!roomDirectory.exists()) {
|
||||||
|
roomDirectory.mkdir()
|
||||||
|
}
|
||||||
|
|
||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
config.staticFiles.add("/public")
|
config.staticFiles.add("/public")
|
||||||
}.get("/") { ctx ->
|
|
||||||
ctx.redirect("/index.html")
|
|
||||||
}
|
}
|
||||||
.get("/api/createaccount/{content}") { ctx -> ctx.result(createAccount(ctx.pathParam("content")))}
|
.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("/api/websocket") { ws ->
|
||||||
ws.onConnect { ctx ->
|
ws.onConnect { ctx ->
|
||||||
WsSessionManager.addSession(ctx)
|
WsSessionManager.addSession(ctx)
|
||||||
|
@ -369,27 +829,52 @@ fun main(args: Array<String>) {
|
||||||
ws.onMessage { ctx ->
|
ws.onMessage { ctx ->
|
||||||
when (ctx.message()) {
|
when (ctx.message()) {
|
||||||
"pong" -> {}
|
"pong" -> {}
|
||||||
else -> {
|
else -> {
|
||||||
println(ctx.message())
|
println(ctx.message())
|
||||||
val successState = handleSentMessage(ctx.message())
|
val successState = handleSentMessage(ctx.message())
|
||||||
if (successState != "Success") {
|
if (successState != "Success") {
|
||||||
try {
|
try {
|
||||||
ctx.send(successState)
|
ctx.send(successState)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Error sending error message: ${e.message}")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val messageContent = extractMessageContent(ctx.message(), ctx)
|
|
||||||
WsSessionManager.broadcast(messageContent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.start(7070)
|
.start(7070)
|
||||||
println("Type a command for the server")
|
|
||||||
while (1 == 1) {
|
try {
|
||||||
println(handleServerCommand(readln()))
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user