229 lines
6.2 KiB
TypeScript
229 lines
6.2 KiB
TypeScript
import { SHA512_256, type ServerWebSocket } from "bun";
|
|
import { SnowflakeGenerator } from "./snowflake.ts";
|
|
|
|
function getContentType(path: string): string {
|
|
if (path.endsWith(".html")) return "text/html";
|
|
if (path.endsWith(".css")) return "text/css";
|
|
if (path.endsWith(".js")) return "application/javascript";
|
|
if (path.endsWith(".json")) return "application/json";
|
|
if (path.endsWith(".png")) return "image/png";
|
|
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
|
|
if (path.endsWith(".svg")) return "image/svg+xml";
|
|
if (path.endsWith(".ico")) return "image/x-icon";
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
enum OpcodesClientbound {
|
|
authorizeLogin,
|
|
initMessages,
|
|
updateMessages,
|
|
registerSuccess,
|
|
error,
|
|
users,
|
|
keepAlive = 9,
|
|
}
|
|
|
|
enum OpcodesServerbound {
|
|
identify,
|
|
sendMessage,
|
|
register,
|
|
getUsers,
|
|
keepAlive = 9,
|
|
}
|
|
|
|
interface user {
|
|
name: string;
|
|
username: string;
|
|
pfp: string;
|
|
id: number;
|
|
token: string;
|
|
password: string;
|
|
dev: boolean;
|
|
}
|
|
|
|
interface message {
|
|
id: string;
|
|
author: user;
|
|
content: string;
|
|
timestamp: number;
|
|
editedTimestamp?: number;
|
|
replyTo?: string;
|
|
hasSent: true; // client sends false but server should always say true
|
|
}
|
|
|
|
const messages: message[] = await Bun.file("./db/messages.json").json();
|
|
|
|
const users: user[] = await Bun.file("./db/users.json").json();
|
|
|
|
const generator = new SnowflakeGenerator();
|
|
|
|
function sendError(ws: ServerWebSocket, reason: string) {
|
|
ws.send(
|
|
JSON.stringify({
|
|
op: OpcodesClientbound.error,
|
|
data: {
|
|
reason,
|
|
},
|
|
})
|
|
);
|
|
ws.close();
|
|
}
|
|
|
|
const server = Bun.serve({
|
|
port: 80,
|
|
websocket: {
|
|
open(ws) {
|
|
console.log("connected to ws");
|
|
ws.subscribe("messages");
|
|
// @ts-expect-error
|
|
ws.interval = setInterval(() => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(
|
|
JSON.stringify({
|
|
op: OpcodesClientbound.keepAlive,
|
|
data: null,
|
|
})
|
|
);
|
|
console.log("sending keeaplivve");
|
|
} else {
|
|
// @ts-expect-error
|
|
clearInterval(ws.interval);
|
|
}
|
|
// @ts-expect-error guhh
|
|
clearTimeout(ws.pingTimeout);
|
|
// @ts-expect-error guhh
|
|
ws.pingTimeout = setTimeout(() => {
|
|
ws.close();
|
|
}, 30_000);
|
|
|
|
}, 60_000);
|
|
// @ts-expect-error guhh
|
|
ws.pingTimeout = null;
|
|
},
|
|
message(ws, msg) {
|
|
const obj = JSON.parse(msg.toString());
|
|
let user: string | any[];
|
|
switch (obj.op) {
|
|
case OpcodesServerbound.identify:
|
|
user = users.filter(user => user.token === obj.data.token);
|
|
if (!(user && user.length !== 0)) {
|
|
user = users.filter(
|
|
(user) =>
|
|
user.password ===
|
|
new SHA512_256().update(obj.data.password ?? "").digest("base64")
|
|
);
|
|
}
|
|
if (user && user.length !== 0) {
|
|
ws.send(
|
|
JSON.stringify({
|
|
op: OpcodesClientbound.authorizeLogin,
|
|
data: user[0],
|
|
})
|
|
);
|
|
ws.send(
|
|
JSON.stringify({
|
|
op: OpcodesClientbound.initMessages,
|
|
data: messages,
|
|
})
|
|
);
|
|
} else {
|
|
sendError(ws, "Not a real user");
|
|
}
|
|
break;
|
|
case OpcodesServerbound.sendMessage:
|
|
user = users.filter((user) => user.token === obj.data.token) as user[];
|
|
if (user && user.length !== 0) {
|
|
obj.op = OpcodesClientbound.updateMessages;
|
|
obj.data.id = generator.generateSnowflake();
|
|
obj.data.author = JSON.parse(JSON.stringify(user[0]));
|
|
obj.data.author.password = null;
|
|
obj.data.author.token = null;
|
|
obj.data.hasSent = true;
|
|
obj.data.token = null;
|
|
obj.data.author.dev = user[0].dev;
|
|
ws.publish("messages", JSON.stringify(obj));
|
|
ws.send(JSON.stringify(obj));
|
|
messages.push(obj.data);
|
|
Bun.write("./db/messages.json", JSON.stringify(messages));
|
|
} else {
|
|
sendError(ws, "Not a real user");
|
|
}
|
|
break;
|
|
case OpcodesServerbound.getUsers:
|
|
let mmiow = JSON.parse(JSON.stringify(users)) as user[];
|
|
mmiow.forEach(user => {
|
|
// @ts-expect-error shush
|
|
user.token = null;
|
|
// @ts-expect-error shush
|
|
user.password = null;
|
|
})
|
|
ws.send(JSON.stringify({
|
|
op: OpcodesClientbound.users,
|
|
data: mmiow
|
|
}))
|
|
break;
|
|
case OpcodesServerbound.keepAlive:
|
|
// @ts-expect-error guhh
|
|
clearTimeout(ws.pingTimeout);
|
|
// @ts-expect-error guhh
|
|
ws.pingTimeout = setTimeout(() => {
|
|
ws.close();
|
|
}, 90_000);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
close(ws) {
|
|
console.log("client disconnected");
|
|
},
|
|
},
|
|
async fetch(req, server) {
|
|
const url = new URL(req.url);
|
|
let path = url.pathname;
|
|
|
|
if (path === "/") {
|
|
path = "/index.html";
|
|
}
|
|
|
|
if (url.pathname === "/ws") {
|
|
const upgraded = server.upgrade(req, {
|
|
// @ts-expect-error guhh whqt
|
|
data: {
|
|
name: new URL(req.url).searchParams.get("name"),
|
|
},
|
|
});
|
|
if (!upgraded) {
|
|
return new Response("Upgrade failed", { status: 400 });
|
|
}
|
|
return;
|
|
}
|
|
|
|
path = path.replace(/\.\./g, "");
|
|
|
|
try {
|
|
const file = Bun.file(`./dist${path}`);
|
|
if (!(await file.exists())) {
|
|
return new Response(
|
|
"<center><h1>404 Not Found</h1><hr><p>Bun</p></center>",
|
|
{ status: 404, headers: { "Content-Type": "text/html" } }
|
|
);
|
|
}
|
|
|
|
return new Response(file, {
|
|
headers: {
|
|
"Content-Type": getContentType(path),
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.error("Error serving file:", err);
|
|
return new Response(
|
|
"<center><h1>500 Internal Server Error</h1><hr><p>Bun</p></center>",
|
|
{ status: 500, headers: { "Content-Type": "text/html" } }
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
console.log(`Server running at http://localhost:${server.port}`);
|