Initial commit: ANOUMA website with Payload CMS, WebRTC meetings, and booking system
- Next.js 16 App Router site with the ANOUMA design system - Payload CMS (PostgreSQL) for offers, events, posts and page content - WebRTC video-call system with custom signaling server - SMTP email reminders and booking-request notifications - Customer accounts, calendar-based availability, and booking workflow
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import type { WebSocket, WebSocketServer } from "ws";
|
||||
import { verifyMeetingToken } from "./token";
|
||||
import {
|
||||
broadcast,
|
||||
getParticipant,
|
||||
isKicked,
|
||||
joinRoom,
|
||||
kickFromRoom,
|
||||
leaveRoom,
|
||||
type Participant,
|
||||
} from "./rooms";
|
||||
import type { ClientToServerMessage, ParticipantSummary } from "./protocol";
|
||||
|
||||
function participantSummary(p: Participant): ParticipantSummary {
|
||||
return { participantId: p.participantId, name: p.name, role: p.role };
|
||||
}
|
||||
|
||||
export function attachSignalingServer(wss: WebSocketServer) {
|
||||
wss.on("connection", (ws: WebSocket, req: IncomingMessage) => {
|
||||
const url = new URL(req.url ?? "", "http://internal");
|
||||
const token = url.searchParams.get("token") ?? "";
|
||||
const payload = verifyMeetingToken(token);
|
||||
|
||||
if (!payload) {
|
||||
ws.close(4000, "invalid-token");
|
||||
return;
|
||||
}
|
||||
if (isKicked(payload.eventSlug, payload.participantId)) {
|
||||
ws.close(4001, "removed-by-host");
|
||||
return;
|
||||
}
|
||||
|
||||
const self: Participant = {
|
||||
ws,
|
||||
participantId: payload.participantId,
|
||||
name: payload.name,
|
||||
role: payload.role,
|
||||
};
|
||||
|
||||
const existing = joinRoom(payload.eventSlug, self);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "welcome",
|
||||
participantId: self.participantId,
|
||||
role: self.role,
|
||||
participants: existing.map(participantSummary),
|
||||
}),
|
||||
);
|
||||
|
||||
broadcast(payload.eventSlug, { type: "peer-joined", ...participantSummary(self) }, {
|
||||
exclude: self.participantId,
|
||||
});
|
||||
|
||||
ws.on("message", (raw) => {
|
||||
let message: ClientToServerMessage;
|
||||
try {
|
||||
message = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "offer" || message.type === "answer" || message.type === "ice-candidate") {
|
||||
const target = getParticipant(payload.eventSlug, message.to);
|
||||
if (target && target.ws.readyState === target.ws.OPEN) {
|
||||
target.ws.send(
|
||||
JSON.stringify({ type: message.type, from: self.participantId, payload: message.payload }),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "kick") {
|
||||
if (self.role !== "host") return; // server-side authority, not client-claimed
|
||||
const target = getParticipant(payload.eventSlug, message.targetParticipantId);
|
||||
if (!target) return;
|
||||
kickFromRoom(payload.eventSlug, target.participantId);
|
||||
target.ws.send(JSON.stringify({ type: "kicked" }));
|
||||
target.ws.close(4001, "removed-by-host");
|
||||
broadcast(payload.eventSlug, { type: "peer-left", participantId: target.participantId });
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
leaveRoom(payload.eventSlug, self.participantId);
|
||||
broadcast(payload.eventSlug, { type: "peer-left", participantId: self.participantId });
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user