136 lines
3.8 KiB
TypeScript
Executable File
136 lines
3.8 KiB
TypeScript
Executable File
import debug from "debug";
|
|
import express from "express";
|
|
import http from "http";
|
|
import { Server as SocketIO } from "socket.io";
|
|
|
|
type UserToFollow = { clientId: string; username: string };
|
|
type OnUserFollowedPayload = {
|
|
userToFollow: UserToFollow;
|
|
action: "follow" | "unfollow";
|
|
};
|
|
|
|
const serverDebug = debug("server");
|
|
const ioDebug = debug("io");
|
|
const socketDebug = debug("socket");
|
|
|
|
require("dotenv").config(
|
|
process.env.NODE_ENV !== "development"
|
|
? { path: ".env.production" }
|
|
: { path: ".env.development" },
|
|
);
|
|
|
|
const app = express();
|
|
const port =
|
|
process.env.PORT || (process.env.NODE_ENV !== "development" ? 80 : 3002); // default port to listen
|
|
|
|
app.use(express.static("public"));
|
|
|
|
app.get("/", (req, res) => {
|
|
res.send("Excalidraw collaboration server is up :)");
|
|
});
|
|
|
|
const server = http.createServer(app);
|
|
|
|
server.listen(port, () => {
|
|
serverDebug(`listening on port: ${port}`);
|
|
});
|
|
|
|
try {
|
|
const io = new SocketIO(server, {
|
|
transports: ["websocket", "polling"],
|
|
cors: {
|
|
allowedHeaders: ["Content-Type", "Authorization"],
|
|
origin: process.env.CORS_ORIGIN || "*",
|
|
credentials: true,
|
|
},
|
|
allowEIO3: true,
|
|
});
|
|
|
|
io.on("connection", (socket) => {
|
|
ioDebug("connection established!");
|
|
io.to(`${socket.id}`).emit("init-room");
|
|
socket.on("join-room", async (roomID) => {
|
|
socketDebug(`${socket.id} has joined ${roomID}`);
|
|
await socket.join(roomID);
|
|
const sockets = await io.in(roomID).fetchSockets();
|
|
if (sockets.length <= 1) {
|
|
io.to(`${socket.id}`).emit("first-in-room");
|
|
} else {
|
|
socketDebug(`${socket.id} new-user emitted to room ${roomID}`);
|
|
socket.broadcast.to(roomID).emit("new-user", socket.id);
|
|
}
|
|
|
|
io.in(roomID).emit(
|
|
"room-user-change",
|
|
sockets.map((socket) => socket.id),
|
|
);
|
|
});
|
|
|
|
socket.on(
|
|
"server-broadcast",
|
|
(roomID: string, encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
|
socketDebug(`${socket.id} sends update to ${roomID}`);
|
|
socket.broadcast.to(roomID).emit("client-broadcast", encryptedData, iv);
|
|
},
|
|
);
|
|
|
|
socket.on(
|
|
"server-volatile-broadcast",
|
|
(roomID: string, encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
|
socketDebug(`${socket.id} sends volatile update to ${roomID}`);
|
|
socket.volatile.broadcast
|
|
.to(roomID)
|
|
.emit("client-broadcast", encryptedData, iv);
|
|
},
|
|
);
|
|
|
|
socket.on("on-user-follow", async (payload: OnUserFollowedPayload) => {
|
|
const roomID = `follow_${payload.userToFollow.clientId}`;
|
|
switch (payload.action) {
|
|
case "follow":
|
|
await socket.join(roomID);
|
|
const sockets = await io.in(roomID).fetchSockets();
|
|
|
|
if (sockets.length === 1) {
|
|
io.to(payload.userToFollow.clientId).emit("broadcast-follow");
|
|
}
|
|
|
|
break;
|
|
case "unfollow":
|
|
await socket.leave(roomID);
|
|
const _sockets = await io.in(roomID).fetchSockets();
|
|
|
|
if (_sockets.length === 0) {
|
|
io.to(payload.userToFollow.clientId).emit("broadcast-unfollow");
|
|
}
|
|
|
|
break;
|
|
}
|
|
});
|
|
|
|
// TODO follow-mode unfollow on disconnect?
|
|
socket.on("disconnecting", async () => {
|
|
socketDebug(`${socket.id} has disconnected`);
|
|
for (const roomID in socket.rooms) {
|
|
const otherClients = (await io.in(roomID).fetchSockets()).filter(
|
|
(_socket) => _socket.id !== socket.id,
|
|
);
|
|
|
|
if (otherClients.length > 0) {
|
|
socket.broadcast.to(roomID).emit(
|
|
"room-user-change",
|
|
otherClients.map((socket) => socket.id),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on("disconnect", () => {
|
|
socket.removeAllListeners();
|
|
socket.disconnect();
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|