improve websocket typing
This commit is contained in:
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
34
.prettierrc
Normal file
34
.prettierrc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"objectWrap": "preserve",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semi": true,
|
||||||
|
"experimentalOperatorPosition": "end",
|
||||||
|
"experimentalTernaries": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"vueIndentScriptAndStyle": true,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"insertPragma": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-svelte"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requirePragma": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"embeddedLanguageFormatting": "auto"
|
||||||
|
}
|
||||||
6
bun.lock
6
bun.lock
@@ -23,6 +23,8 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/polka": "^0.5.7",
|
"@types/polka": "^0.5.7",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
@@ -357,6 +359,10 @@
|
|||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
|
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||||
|
|||||||
88
package.json
88
package.json
@@ -1,43 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "wormhole",
|
"name": "wormhole",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
},
|
"format": "prettier --write .",
|
||||||
"devDependencies": {
|
"lint": "prettier --check ."
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
},
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
"@types/polka": "^0.5.7",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"svelte": "^5.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"@types/polka": "^0.5.7",
|
||||||
"tailwindcss": "^4.0.0",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.0.0",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"vite": "^7.0.4"
|
"svelte": "^5.0.0",
|
||||||
},
|
"svelte-check": "^4.0.0",
|
||||||
"dependencies": {
|
"tailwindcss": "^4.0.0",
|
||||||
"@hpke/chacha20poly1305": "^1.7.1",
|
"typescript": "^5.0.0",
|
||||||
"@hpke/core": "^1.7.4",
|
"vite": "^7.0.4"
|
||||||
"@hpke/hybridkem-x-wing": "^0.6.1",
|
},
|
||||||
"@noble/ciphers": "^1.3.0",
|
"dependencies": {
|
||||||
"@noble/curves": "^1.9.0",
|
"@hpke/chacha20poly1305": "^1.7.1",
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@hpke/core": "^1.7.4",
|
||||||
"@types/streamsaver": "^2.0.5",
|
"@hpke/hybridkem-x-wing": "^0.6.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@noble/ciphers": "^1.3.0",
|
||||||
"polka": "^0.5.2",
|
"@noble/curves": "^1.9.0",
|
||||||
"streamsaver": "^2.0.6",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
"ts-mls": "^1.1.0",
|
"@types/streamsaver": "^2.0.5",
|
||||||
"ws": "^8.18.3"
|
"@types/ws": "^8.18.1",
|
||||||
},
|
"polka": "^0.5.2",
|
||||||
"trustedDependencies": [
|
"streamsaver": "^2.0.6",
|
||||||
"@tailwindcss/oxide"
|
"ts-mls": "^1.1.0",
|
||||||
]
|
"ws": "^8.18.3"
|
||||||
}
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@tailwindcss/oxide"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { ws } from "$stores/websocketStore";
|
import { ws } from "$stores/websocketStore";
|
||||||
import { WebSocketMessageType } from "$types/websocket";
|
import { WebSocketRequestType, WebSocketResponseType } from "$types/websocket";
|
||||||
import { solveChallenge } from "./powUtil";
|
import { solveChallenge } from "./powUtil";
|
||||||
|
|
||||||
export async function doChallenge(additionalData: string = ""): Promise<{
|
export async function doChallenge(additionalData: string = ""): Promise<{
|
||||||
challenge: string;
|
target: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
} | null> {
|
} | null> {
|
||||||
let roomChallenge: string | null = null;
|
let roomChallengeTarget: string | null = null;
|
||||||
|
|
||||||
let challengePromise = new Promise<string | null>((resolve) => {
|
let challengePromise = new Promise<string | null>((resolve) => {
|
||||||
let unsubscribe = ws.handleEvent(
|
let unsubscribe = ws.handleEvent(
|
||||||
WebSocketMessageType.CHALLENGE,
|
WebSocketResponseType.CHALLENGE_RESPONSE,
|
||||||
async (value) => {
|
async (value) => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
roomChallenge = value.challenge;
|
roomChallengeTarget = value.target;
|
||||||
resolve(
|
resolve(
|
||||||
await solveChallenge(
|
await solveChallenge(
|
||||||
roomChallenge,
|
roomChallengeTarget,
|
||||||
value.difficulty,
|
value.difficulty,
|
||||||
additionalData,
|
additionalData,
|
||||||
),
|
),
|
||||||
@@ -26,7 +26,7 @@ export async function doChallenge(additionalData: string = ""): Promise<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.REQUEST_CHALLENGE,
|
type: WebSocketRequestType.CHALLENGE_REQUEST,
|
||||||
});
|
});
|
||||||
|
|
||||||
let challengeNonce = await challengePromise;
|
let challengeNonce = await challengePromise;
|
||||||
@@ -34,12 +34,12 @@ export async function doChallenge(additionalData: string = ""): Promise<{
|
|||||||
throw new Error("Could not solve challenge within max iterations");
|
throw new Error("Could not solve challenge within max iterations");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roomChallenge) {
|
if (!roomChallengeTarget) {
|
||||||
throw new Error("No room challenge");
|
throw new Error("No room challenge");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challenge: roomChallenge,
|
target: roomChallengeTarget,
|
||||||
nonce: challengeNonce,
|
nonce: challengeNonce,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../../types/websocket.ts";
|
import { RoomStatusType, Socket, WebSocketErrorType, WebSocketRequestType, WebSocketResponseType, WebSocketRoomMessageType, WebSocketWebRtcMessageType, type WebSocketMessage } from "../../types/websocket.ts";
|
||||||
import { LiveMap } from '../liveMap.ts';
|
import { LiveMap } from '../liveMap.ts';
|
||||||
import { hashStringSHA256 } from "../powUtil.ts";
|
import { hashStringSHA256 } from "../powUtil.ts";
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ async function createRoom(socket: Socket, roomName?: string): Promise<string> {
|
|||||||
|
|
||||||
let room = rooms.set(roomId, new ServerRoom());
|
let room = rooms.set(roomId, new ServerRoom());
|
||||||
|
|
||||||
socket.send({ type: WebSocketMessageType.ROOM_CREATED, data: room.key });
|
socket.send({ type: WebSocketResponseType.ROOM_CREATED, data: room.key });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await joinRoom(room.key, socket, true);
|
await joinRoom(room.key, socket, true);
|
||||||
@@ -86,21 +86,21 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
|
|||||||
|
|
||||||
// should be unreachable
|
// should be unreachable
|
||||||
if (!room) {
|
if (!room) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.length == 2) {
|
if (room.length == 2) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_FULL });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_FULL });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify all clients in the room of the new client, except the client itself
|
// notify all clients in the room of the new client, except the client itself
|
||||||
room.notifyAll({ type: WebSocketMessageType.JOIN_ROOM, roomId });
|
room.notifyAll({ type: WebSocketRoomMessageType.PARTICIPANT_JOINED, roomId, participants: room.length });
|
||||||
room.push(socket);
|
room.push(socket);
|
||||||
|
|
||||||
socket.addEventListener('close', (ev) => {
|
socket.addEventListener('close', (ev) => {
|
||||||
room.notifyAll({ type: WebSocketMessageType.ROOM_LEFT, roomId });
|
room.notifyAll({ type: WebSocketRoomMessageType.PARTICIPANT_LEFT, roomId, participants: room.length });
|
||||||
|
|
||||||
// for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1
|
// for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1
|
||||||
// then when this client disconnects, the room should be deleted since the room is empty
|
// then when this client disconnects, the room should be deleted since the room is empty
|
||||||
@@ -118,12 +118,13 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
|
|||||||
room.set(room.filter(client => client.ws !== ev.target));
|
room.set(room.filter(client => client.ws !== ev.target));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// sending the join message to the client who created the room is fucky
|
||||||
if (!initial) {
|
if (!initial) {
|
||||||
socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: roomId, participants: room.length });
|
socket.send({ type: WebSocketResponseType.ROOM_JOINED, roomId: roomId, participants: room.length });
|
||||||
}
|
}
|
||||||
// TODO: consider letting rooms get larger than 2 clients
|
// TODO: consider letting rooms get larger than 2 clients
|
||||||
if (room.length == 2) {
|
if (room.length == 2) {
|
||||||
room.forEachClient(client => client.send({ type: WebSocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } }));
|
room.forEachClient(client => client.send({ type: WebSocketRoomMessageType.ROOM_READY, data: { isInitiator: client !== socket, roomId, participants: room.length } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Room created:", roomId, room.length);
|
console.log("Room created:", roomId, room.length);
|
||||||
@@ -148,17 +149,17 @@ function generateChallenge(): string {
|
|||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateChallenge(challenge: string, nonce: string, additionalData: string = ""): Promise<boolean> {
|
async function validateChallenge(challenge: {target: string, nonce: string}, additionalData: string = ""): Promise<boolean> {
|
||||||
if (!outstandingChallenges.has(challenge)) {
|
if (!outstandingChallenges.has(challenge.target)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = await hashStringSHA256(`${additionalData}${challenge}${nonce}`);
|
let hash = await hashStringSHA256(`${additionalData}${challenge.target}${challenge.nonce}`);
|
||||||
let result = hash.startsWith('0'.repeat(CHALLENGE_DIFFICULTY));
|
let result = hash.startsWith('0'.repeat(CHALLENGE_DIFFICULTY));
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log("Challenge solved:", challenge);
|
console.log("Challenge solved:", challenge);
|
||||||
clearTimeout(outstandingChallenges.get(challenge)!);
|
clearTimeout(outstandingChallenges.get(challenge.target)!);
|
||||||
outstandingChallenges.delete(challenge);
|
outstandingChallenges.delete(challenge.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -170,7 +171,7 @@ function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
|
|||||||
|
|
||||||
// should be unreachable
|
// should be unreachable
|
||||||
if (!room) {
|
if (!room) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
|
|||||||
|
|
||||||
room.set(room.filter(client => client !== socket));
|
room.set(room.filter(client => client !== socket));
|
||||||
|
|
||||||
socket.send({ type: WebSocketMessageType.ROOM_LEFT, roomId });
|
socket.send({ type: WebSocketResponseType.ROOM_LEFT, roomId });
|
||||||
|
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
@@ -217,7 +218,7 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
console.log("Received non-JSON message:", event);
|
console.log("Received non-JSON message:", event);
|
||||||
// If the message is not JSON, send an error message
|
// If the message is not JSON, send an error message
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MALFORMED_MESSAGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,14 +227,14 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
let room: ServerRoom | undefined = undefined;
|
let room: ServerRoom | undefined = undefined;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.CREATE_ROOM:
|
case WebSocketRequestType.CREATE_ROOM:
|
||||||
if (!message.nonce || !message.challenge) {
|
if (!message.challenge || !message.challenge.target || !message.challenge.nonce) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MISSING_DATA });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await validateChallenge(message.challenge, message.nonce)) {
|
if (!await validateChallenge(message.challenge)) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.INVALID_CHALLENGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,23 +252,23 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
|
|
||||||
await createRoom(socket, message.roomName);
|
await createRoom(socket, message.roomName);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: e.message });
|
socket.send({ type: WebSocketErrorType.ERROR, data: e.message });
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.JOIN_ROOM:
|
case WebSocketRequestType.ROOM_JOIN:
|
||||||
if (!message.roomId || !message.nonce || !message.challenge) {
|
if (!message.roomId || !message.challenge || !message.challenge.target || !message.challenge.nonce) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MISSING_DATA });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await validateChallenge(message.challenge, message.nonce, message.roomId)) {
|
if (!await validateChallenge(message.challenge, message.roomId)) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.INVALID_CHALLENGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rooms.get(message.roomId) == undefined) {
|
if (rooms.get(message.roomId) == undefined) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,14 +276,14 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.LEAVE_ROOM:
|
case WebSocketRequestType.ROOM_LEAVE:
|
||||||
if (!message.roomId) {
|
if (!message.roomId) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MALFORMED_MESSAGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rooms.get(message.roomId) == undefined) {
|
if (rooms.get(message.roomId) == undefined) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,27 +291,34 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.CHECK_ROOM_EXISTS:
|
case WebSocketRequestType.ROOM_STATUS:
|
||||||
if (!message.roomId || !message.nonce || !message.challenge) {
|
if (!message.roomId || !message.challenge || !message.challenge.target || !message.challenge.nonce) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MISSING_DATA });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await validateChallenge(message.challenge, message.nonce, message.roomId)) {
|
if (!await validateChallenge(message.challenge, message.roomId)) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.INVALID_CHALLENGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.send({ type: WebSocketMessageType.ROOM_STATUS, roomId: message.roomId, status: rooms.get(message.roomId) ? 'found' : 'not-found' });
|
let roomStatus = RoomStatusType.OPEN;
|
||||||
break;
|
if (!rooms.get(message.roomId)) {
|
||||||
case WebSocketMessageType.REQUEST_CHALLENGE:
|
roomStatus = RoomStatusType.NOT_FOUND;
|
||||||
let challenge = generateChallenge();
|
} else if (rooms.get(message.roomId)!.length === 2) {
|
||||||
|
roomStatus = RoomStatusType.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
socket.send({ type: WebSocketMessageType.CHALLENGE, challenge, difficulty: CHALLENGE_DIFFICULTY });
|
socket.send({ type: WebSocketResponseType.ROOM_STATUS, roomId: message.roomId, status: roomStatus });
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.WEBRTC_OFFER:
|
case WebSocketRequestType.CHALLENGE_REQUEST:
|
||||||
case WebSocketMessageType.WERTC_ANSWER:
|
let target = generateChallenge();
|
||||||
case WebSocketMessageType.WEBRTC_ICE_CANDIDATE:
|
|
||||||
|
socket.send({ type: WebSocketResponseType.CHALLENGE_RESPONSE, target, difficulty: CHALLENGE_DIFFICULTY });
|
||||||
|
break;
|
||||||
|
case WebSocketWebRtcMessageType.OFFER:
|
||||||
|
case WebSocketWebRtcMessageType.ANSWER:
|
||||||
|
case WebSocketWebRtcMessageType.ICE_CANDIDATE:
|
||||||
// relay these messages to the other peers in the room
|
// relay these messages to the other peers in the room
|
||||||
room = rooms.get(message.data.roomId);
|
room = rooms.get(message.data.roomId);
|
||||||
|
|
||||||
@@ -324,7 +332,7 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown message type: ${message.type}`);
|
console.warn(`Unknown message type: ${message.type}`);
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.UNKNOWN_MESSAGE_TYPE });
|
socket.send({ type: WebSocketErrorType.ERROR, data: errors.UNKNOWN_MESSAGE_TYPE });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
import { ws } from '$stores/websocketStore';
|
import { ws } from '$stores/websocketStore';
|
||||||
import { WebSocketMessageType } from '$types/websocket';
|
|
||||||
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '$types/webrtc';
|
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '$types/webrtc';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createApplicationMessage, createCommit, createGroup, decodeMlsMessage, defaultCapabilities, defaultLifetime, emptyPskIndex, encodeMlsMessage, generateKeyPackage, getCiphersuiteFromName, getCiphersuiteImpl, joinGroup, processPrivateMessage, type CiphersuiteImpl, type ClientState, type Credential, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
|
import { createApplicationMessage, createCommit, createGroup, decodeMlsMessage, defaultCapabilities, defaultLifetime, emptyPskIndex, encodeMlsMessage, generateKeyPackage, getCiphersuiteFromName, getCiphersuiteImpl, joinGroup, processPrivateMessage, type CiphersuiteImpl, type ClientState, type Credential, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
|
||||||
|
import { WebSocketWebRtcMessageType } from '$types/websocket';
|
||||||
|
|
||||||
export class WebRTCPeer {
|
export class WebRTCPeer {
|
||||||
private peer: RTCPeerConnection | null = null;
|
private peer: RTCPeerConnection | null = null;
|
||||||
@@ -35,7 +35,7 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
private sendIceCandidate(candidate: RTCIceCandidate) {
|
private sendIceCandidate(candidate: RTCIceCandidate) {
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE,
|
type: WebSocketWebRtcMessageType.ICE_CANDIDATE,
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
candidate: candidate,
|
candidate: candidate,
|
||||||
@@ -261,7 +261,7 @@ export class WebRTCPeer {
|
|||||||
await this.peer.setLocalDescription(offer)
|
await this.peer.setLocalDescription(offer)
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.WEBRTC_OFFER,
|
type: WebSocketWebRtcMessageType.OFFER,
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
sdp: offer,
|
sdp: offer,
|
||||||
@@ -295,7 +295,7 @@ export class WebRTCPeer {
|
|||||||
console.log("Sending answer", answer);
|
console.log("Sending answer", answer);
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.WERTC_ANSWER,
|
type: WebSocketWebRtcMessageType.ANSWER,
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
sdp: answer,
|
sdp: answer,
|
||||||
@@ -353,7 +353,7 @@ export class WebRTCPeer {
|
|||||||
this.send(keyPackageMessageBuf, WebRTCPacketType.KEY_PACKAGE);
|
this.send(keyPackageMessageBuf, WebRTCPacketType.KEY_PACKAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(data: ArrayBuffer, type: WebRTCPacketType) {
|
public async send(data: ArrayBufferLike, type: WebRTCPacketType) {
|
||||||
console.log("Sending message of type", type, "with data", data);
|
console.log("Sending message of type", type, "with data", data);
|
||||||
|
|
||||||
if (!this.dataChannel || this.dataChannel.readyState !== 'open') throw new Error('Data channel not initialized');
|
if (!this.dataChannel || this.dataChannel.readyState !== 'open') throw new Error('Data channel not initialized');
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { writable, get, type Writable } from "svelte/store";
|
|||||||
import { WebRTCPeer } from "$lib/webrtc";
|
import { WebRTCPeer } from "$lib/webrtc";
|
||||||
import { WebRTCPacketType } from "$types/webrtc";
|
import { WebRTCPacketType } from "$types/webrtc";
|
||||||
import { room } from "$stores/roomStore";
|
import { room } from "$stores/roomStore";
|
||||||
import { RoomConnectionState, type Room } from "$types/websocket";
|
import { RoomConnectionState, WebSocketErrorType, WebSocketResponseType, WebSocketRoomMessageType, WebSocketWebRtcMessageType, type Room } from "$types/websocket";
|
||||||
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "$stores/messageStore";
|
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "$stores/messageStore";
|
||||||
import { MessageType, type Message } from "$types/message";
|
import { MessageType, type Message } from "$types/message";
|
||||||
import { WebSocketMessageType, type WebSocketMessage } from "$types/websocket";
|
import { type WebSocketMessage } from "$types/websocket";
|
||||||
import { WebBuffer } from "./buffer";
|
import { WebBuffer } from "./buffer";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
@@ -257,30 +257,30 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
const message: WebSocketMessage = JSON.parse(event.data);
|
const message: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.ROOM_CREATED:
|
case WebSocketResponseType.ROOM_CREATED:
|
||||||
console.log("Room created:", message.data);
|
console.log("Room created:", message.data);
|
||||||
room.set({ id: message.data, host: true, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: 1 });
|
room.set({ id: message.data, host: true, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: 1 });
|
||||||
goto(`/${message.data}`);
|
goto(`/${message.data}`);
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.JOIN_ROOM:
|
case WebSocketRoomMessageType.PARTICIPANT_JOINED:
|
||||||
console.log("new client joined room");
|
console.log("new client joined room");
|
||||||
room.update((room) => ({ ...room, participants: room.participants + 1 }));
|
room.update((room) => ({ ...room, participants: room.participants + 1 }));
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ROOM_JOINED:
|
case WebSocketResponseType.ROOM_JOINED:
|
||||||
// TODO: if a client disconnects, we need to resync the room state
|
// TODO: if a client disconnects, we need to resync the room state
|
||||||
|
|
||||||
room.set({ host: false, id: message.roomId, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: message.participants });
|
room.set({ host: false, id: message.roomId, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: message.participants });
|
||||||
console.log("Joined room");
|
console.log("Joined room");
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ROOM_LEFT:
|
case WebSocketRoomMessageType.PARTICIPANT_LEFT:
|
||||||
room.update((room) => ({ ...room, participants: room.participants - 1 }));
|
room.update((room) => ({ ...room, participants: room.participants - 1 }));
|
||||||
console.log("Participant left room");
|
console.log("Participant left room");
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ERROR:
|
case WebSocketErrorType.ERROR:
|
||||||
console.error("Error:", message.data);
|
console.error("Error:", message.data);
|
||||||
error.set(message.data);
|
error.set(message.data);
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ROOM_READY:
|
case WebSocketRoomMessageType.ROOM_READY:
|
||||||
let roomId = get(room).id;
|
let roomId = get(room).id;
|
||||||
|
|
||||||
if (roomId === null) {
|
if (roomId === null) {
|
||||||
@@ -307,20 +307,20 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.WEBRTC_OFFER:
|
case WebSocketWebRtcMessageType.OFFER:
|
||||||
console.log("Received offer");
|
console.log("Received offer");
|
||||||
await get(peer)?.setRemoteDescription(
|
await get(peer)?.setRemoteDescription(
|
||||||
new RTCSessionDescription(message.data.sdp),
|
new RTCSessionDescription(message.data.sdp),
|
||||||
);
|
);
|
||||||
await get(peer)?.createAnswer();
|
await get(peer)?.createAnswer();
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.WERTC_ANSWER:
|
case WebSocketWebRtcMessageType.ANSWER:
|
||||||
console.log("Received answer");
|
console.log("Received answer");
|
||||||
await get(peer)?.setRemoteDescription(
|
await get(peer)?.setRemoteDescription(
|
||||||
new RTCSessionDescription(message.data.sdp),
|
new RTCSessionDescription(message.data.sdp),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.WEBRTC_ICE_CANDIDATE:
|
case WebSocketWebRtcMessageType.ICE_CANDIDATE:
|
||||||
console.log("Received ICE candidate");
|
console.log("Received ICE candidate");
|
||||||
await get(peer)?.addIceCandidate(message.data.candidate);
|
await get(peer)?.addIceCandidate(message.data.candidate);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,11 +15,7 @@
|
|||||||
|
|
||||||
ws.subscribe((newWs) => {
|
ws.subscribe((newWs) => {
|
||||||
if (newWs.status === WebsocketConnectionState.CONNECTED) {
|
if (newWs.status === WebsocketConnectionState.CONNECTED) {
|
||||||
console.log(
|
console.log("Connected to websocket server, room id:", $room.id, "reconnecting");
|
||||||
"Connected to websocket server, room id:",
|
|
||||||
$room.id,
|
|
||||||
"reconnecting",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,35 +29,29 @@
|
|||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2"
|
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
|
||||||
/>
|
|
||||||
{#if process.env.NODE_ENV !== "production"}
|
{#if process.env.NODE_ENV !== "production"}
|
||||||
|
<!-- Debug console. Particularly useful for debugging on mobile devices -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
<script>
|
<script>
|
||||||
eruda.init();
|
eruda.init();
|
||||||
</script>
|
</script>
|
||||||
{/if}
|
{/if}
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
|
||||||
></script>
|
<script src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"></script>
|
||||||
<script
|
|
||||||
src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"
|
|
||||||
></script>
|
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<header class="p-5">
|
<header class="p-5">
|
||||||
<div class="flex justify-between items-center max-w-7xl px-5 mx-auto">
|
<div class="flex justify-between items-center max-w-7xl px-5 mx-auto">
|
||||||
<div class="text-2xl font-bold text-white">
|
<div class="text-2xl font-bold text-white">
|
||||||
<a href="/" class="!text-white !no-underline"
|
<a href="/" class="!text-white !no-underline">Noctis<span class="text-accent">.</span>
|
||||||
>Noctis<span class="text-accent">.</span></a
|
</a>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a
|
<a href="https://github.com/juls0730/noctis" target="_blank" rel="noopener noreferrer">
|
||||||
href="https://github.com/juls0730/noctis"
|
GitHub
|
||||||
target="_blank"
|
</a>
|
||||||
rel="noopener noreferrer">GitHub</a
|
|
||||||
>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||||
import { WebSocketMessageType } from "$types/websocket";
|
import { WebSocketRequestType } from "$types/websocket";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import LoadingSpinner from "$components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$components/LoadingSpinner.svelte";
|
||||||
import { doChallenge } from "$lib/challenge";
|
import { doChallenge } from "$lib/challenge";
|
||||||
@@ -18,10 +18,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.CREATE_ROOM,
|
type: WebSocketRequestType.CREATE_ROOM,
|
||||||
roomName: roomId,
|
roomName: roomId,
|
||||||
nonce: challengeResult.nonce,
|
challenge: {
|
||||||
challenge: challengeResult.challenge,
|
target: challengeResult.target,
|
||||||
|
nonce: challengeResult.nonce,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Created room:", roomId);
|
console.log("Created room:", roomId);
|
||||||
@@ -35,72 +37,63 @@
|
|||||||
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
||||||
<h1 class="font-bold">Your Private, Peer-to-Peer Chat Room</h1>
|
<h1 class="font-bold">Your Private, Peer-to-Peer Chat Room</h1>
|
||||||
<p class="max-w-xl mx-8">
|
<p class="max-w-xl mx-8">
|
||||||
End-to-end encrypted. Peer-to-peer. No servers. No sign-ups. Just
|
End-to-end encrypted. Peer-to-peer. No servers. No sign-ups. Just chat.
|
||||||
chat.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-surface p-10 rounded-xl max-w-xl shadow-xl border border-[#21293b] mt-10 mr-auto ml-auto w-full"
|
class="bg-surface p-10 rounded-xl max-w-xl shadow-xl border border-[#21293b] mt-10 mr-auto ml-auto w-full">
|
||||||
>
|
|
||||||
<form class="flex flex-col gap-5" id="roomForm">
|
<form class="flex flex-col gap-5" id="roomForm">
|
||||||
<button
|
<button
|
||||||
onclick={createRoom}
|
onclick={createRoom}
|
||||||
disabled={$ws.status !==
|
disabled={$ws.status !== WebsocketConnectionState.CONNECTED || $roomLoading}
|
||||||
WebsocketConnectionState.CONNECTED || $roomLoading}
|
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20">
|
||||||
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20"
|
|
||||||
>
|
|
||||||
{#if $ws.status !== WebsocketConnectionState.CONNECTED}
|
{#if $ws.status !== WebsocketConnectionState.CONNECTED}
|
||||||
<span class="flex items-center"
|
<span class="flex items-center">
|
||||||
><LoadingSpinner /> Connecting to server...</span
|
<LoadingSpinner /> Connecting to server...
|
||||||
>
|
</span>
|
||||||
{:else if $roomLoading}
|
{:else if $roomLoading}
|
||||||
<span class="flex items-center"
|
<span class="flex items-center">
|
||||||
><LoadingSpinner /> Creating Room...</span
|
<LoadingSpinner /> Creating Room...
|
||||||
>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24">
|
||||||
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||||
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3"
|
d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3" />
|
||||||
/></svg
|
</svg>
|
||||||
>
|
|
||||||
Create Secure Room
|
Create Secure Room
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="{$showRoomNameInput
|
class="{$showRoomNameInput
|
||||||
? 'max-h-32'
|
? 'max-h-32'
|
||||||
: 'max-h-0 opacity-0'} overflow-hidden transition-[max-height,_opacity] duration-700"
|
: 'max-h-0 opacity-0'} overflow-hidden transition-[max-height,_opacity] duration-700">
|
||||||
>
|
|
||||||
<label
|
<label
|
||||||
aria-hidden={!$showRoomNameInput}
|
aria-hidden={!$showRoomNameInput}
|
||||||
for="roomNameInput"
|
for="roomNameInput"
|
||||||
class="text-paragraph block text-sm font-medium mb-2 text-left"
|
class="text-paragraph block text-sm font-medium mb-2 text-left">
|
||||||
>Enter a custom room name</label
|
Enter a custom room name
|
||||||
>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="roomNameInput"
|
id="roomNameInput"
|
||||||
bind:value={$roomName}
|
bind:value={$roomName}
|
||||||
class="placeholder:text-paragraph-muted w-full py-3 px-4 rounded-lg border border-[#2c3444] bg-[#232b3e] text-paragraph transition-[border-color,_box-shadow] duration-300 ease-in-out focus:outline-none focus:border-accent focus:shadow-sm shadow-accent/20"
|
class="placeholder:text-paragraph-muted w-full py-3 px-4 rounded-lg border border-[#2c3444] bg-[#232b3e] text-paragraph transition-[border-color,_box-shadow] duration-300 ease-in-out focus:outline-none focus:border-accent focus:shadow-sm shadow-accent/20"
|
||||||
placeholder="e.g., private-chat"
|
placeholder="e.g., private-chat" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span class="text-paragraph {$showRoomNameInput ? 'hidden' : '-mt-5'}">
|
||||||
class="text-paragraph {$showRoomNameInput
|
or <button
|
||||||
? 'hidden'
|
|
||||||
: '-mt-5'}"
|
|
||||||
>or <button
|
|
||||||
id="showCustomNameLink"
|
id="showCustomNameLink"
|
||||||
class="cursor-pointer underline hover:no-underline text-accent"
|
class="cursor-pointer underline hover:no-underline text-accent"
|
||||||
onclick={() => showRoomNameInput.set(true)}
|
onclick={() => showRoomNameInput.set(true)}>
|
||||||
>choose a custom room name</button
|
choose a custom room name
|
||||||
></span
|
</button>
|
||||||
>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,39 +105,35 @@
|
|||||||
<div class="mt-10 flex justify-around gap-8 flex-wrap">
|
<div class="mt-10 flex justify-around gap-8 flex-wrap">
|
||||||
<div class="text-center max-w-3xs">
|
<div class="text-center max-w-3xs">
|
||||||
<div
|
<div
|
||||||
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
|
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5">
|
||||||
>
|
|
||||||
1
|
1
|
||||||
</div>
|
</div>
|
||||||
<h3>Create a Room</h3>
|
<h3>Create a Room</h3>
|
||||||
<p>
|
<p>
|
||||||
Click the button above to create a random room instantly, no
|
Click the button above to create a random room instantly, no personal info
|
||||||
personal info required.
|
required.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center max-w-3xs">
|
<div class="text-center max-w-3xs">
|
||||||
<div
|
<div
|
||||||
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
|
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5">
|
||||||
>
|
|
||||||
2
|
2
|
||||||
</div>
|
</div>
|
||||||
<h3>Share the Link</h3>
|
<h3>Share the Link</h3>
|
||||||
<p>
|
<p>
|
||||||
You'll get a unique link to your private room. Share this
|
You'll get a unique link to your private room. Share this link with anyone you
|
||||||
link with anyone you want to chat with securely.
|
want to chat with securely.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center max-w-3xs">
|
<div class="text-center max-w-3xs">
|
||||||
<div
|
<div
|
||||||
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
|
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5">
|
||||||
>
|
|
||||||
3
|
3
|
||||||
</div>
|
</div>
|
||||||
<h3>Chat Privately</h3>
|
<h3>Chat Privately</h3>
|
||||||
<p>
|
<p>
|
||||||
Once they join, your messages are sent directly between your
|
Once they join, your messages are sent directly between your devices, encrypted
|
||||||
devices, encrypted from end to end. Hidden from everyone
|
from end to end. Hidden from everyone else.
|
||||||
else.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,93 +143,80 @@
|
|||||||
<section class="py-20">
|
<section class="py-20">
|
||||||
<div class="max-w-6xl px-10 mx-auto">
|
<div class="max-w-6xl px-10 mx-auto">
|
||||||
<h2 class="font-semibold">Security by Design</h2>
|
<h2 class="font-semibold">Security by Design</h2>
|
||||||
<div
|
<div class="mt-10 grid grid-cols-[repeat(auto-fit,_minmax(300px,_1fr))] gap-8">
|
||||||
class="mt-10 grid grid-cols-[repeat(auto-fit,_minmax(300px,_1fr))] gap-8"
|
<div class="bg-surface p-8 rounded-xl border border-[#21293b] text-center">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
|
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph">
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24">
|
||||||
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><g
|
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||||
|
<g
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2">
|
||||||
><path
|
<path
|
||||||
d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z"
|
d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z" />
|
||||||
/><path
|
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
|
||||||
d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4"
|
</g>
|
||||||
/></g
|
</svg>
|
||||||
></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold">End-to-End Encrypted</h3>
|
<h3 class="font-bold">End-to-End Encrypted</h3>
|
||||||
<p>
|
<p>
|
||||||
Only you and the people in your room can read the messages.
|
Only you and the people in your room can read the messages. Your data is
|
||||||
Your data is encrypted before its sent using the Message
|
encrypted before its sent using the Message Layer Security (MLS) protocol.
|
||||||
Layer Security (MLS) protocol.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="bg-surface p-8 rounded-xl border border-[#21293b] text-center">
|
||||||
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
|
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph">
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24">
|
||||||
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||||
|
<path
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M21 11V8a2 2 0 0 0-2-2h-6m0 0l3 3m-3-3l3-3M3 13.013v3a2 2 0 0 0 2 2h6m0 0l-3-3m3 3l-3 3m8-4.511a2 2 0 1 0 4.001-.001a2 2 0 0 0-4.001.001m-12-12a2 2 0 1 0 4.001-.001A2 2 0 0 0 4 4.502m17 16.997a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2m-6-12a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2"
|
d="M21 11V8a2 2 0 0 0-2-2h-6m0 0l3 3m-3-3l3-3M3 13.013v3a2 2 0 0 0 2 2h6m0 0l-3-3m3 3l-3 3m8-4.511a2 2 0 1 0 4.001-.001a2 2 0 0 0-4.001.001m-12-12a2 2 0 1 0 4.001-.001A2 2 0 0 0 4 4.502m17 16.997a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2m-6-12a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2" />
|
||||||
/></svg
|
</svg>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold">Truly Peer-to-Peer</h3>
|
<h3 class="font-bold">Truly Peer-to-Peer</h3>
|
||||||
<p>
|
<p>
|
||||||
Your messages are sent directly from your device to the
|
Your messages are sent directly from your device to the recipient's. They never
|
||||||
recipient's. They never pass through a central server.
|
pass through a central server.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="bg-surface p-8 rounded-xl border border-[#21293b] text-center">
|
||||||
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
|
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph">
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24">
|
||||||
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||||
|
<path
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="m3 3l18 18M7 3h7l5 5v7m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5"
|
d="m3 3l18 18M7 3h7l5 5v7m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5" />
|
||||||
/></svg
|
</svg>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold">No Data Stored</h3>
|
<h3 class="font-bold">No Data Stored</h3>
|
||||||
<p>
|
<p>
|
||||||
We don't have accounts, and we don't store your messages.
|
We don't have accounts, and we don't store your messages. Once you close the
|
||||||
Once you close the tab, the conversation is gone forever.
|
tab, the conversation is gone forever.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,19 +229,19 @@
|
|||||||
© {new Date().getFullYear()} Noctis - MIT License
|
© {new Date().getFullYear()} Noctis - MIT License
|
||||||
<br />
|
<br />
|
||||||
Made with
|
Made with
|
||||||
<span class="text-accent"
|
<span class="text-accent">
|
||||||
><svg
|
<svg
|
||||||
class="inline-block"
|
class="inline-block"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24">
|
||||||
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||||
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M6.979 3.074a6 6 0 0 1 4.988 1.425l.037.033l.034-.03a6 6 0 0 1 4.733-1.44l.246.036a6 6 0 0 1 3.364 10.008l-.18.185l-.048.041l-7.45 7.379a1 1 0 0 1-1.313.082l-.094-.082l-7.493-7.422A6 6 0 0 1 6.979 3.074"
|
d="M6.979 3.074a6 6 0 0 1 4.988 1.425l.037.033l.034-.03a6 6 0 0 1 4.733-1.44l.246.036a6 6 0 0 1 3.364 10.008l-.18.185l-.048.041l-7.45 7.379a1 1 0 0 1-1.313.082l-.094-.082l-7.493-7.422A6 6 0 0 1 6.979 3.074" />
|
||||||
/></svg
|
</svg>
|
||||||
></span
|
</span>
|
||||||
>
|
|
||||||
by
|
by
|
||||||
<a href="https://zoeissleeping.com">zoeissleeping</a>
|
<a href="https://zoeissleeping.com">zoeissleeping</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { room } from "$stores/roomStore";
|
import { room } from "$stores/roomStore";
|
||||||
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||||
import { WebSocketMessageType } from "$types/websocket";
|
import { RoomStatusType, WebSocketRequestType, WebSocketResponseType } from "$types/websocket";
|
||||||
import { dataChannelReady, error } from "$lib/webrtcUtil";
|
import { dataChannelReady, error } from "$lib/webrtcUtil";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import RtcMessage from "$components/RTCMessage.svelte";
|
import RtcMessage from "$components/RTCMessage.svelte";
|
||||||
@@ -47,10 +47,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.JOIN_ROOM,
|
type: WebSocketRequestType.ROOM_JOIN,
|
||||||
roomId: roomId!,
|
roomId: roomId!,
|
||||||
nonce: challengeResult.nonce,
|
challenge: {
|
||||||
challenge: challengeResult.challenge,
|
target: challengeResult.target,
|
||||||
|
nonce: challengeResult.nonce,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleLeave() {
|
function handleLeave() {
|
||||||
if (
|
if (confirm("Are you sure you want to leave? The chat history will be deleted.")) {
|
||||||
confirm(
|
|
||||||
"Are you sure you want to leave? The chat history will be deleted.",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// In a real app, this would disconnect the P2P session and redirect.
|
// In a real app, this would disconnect the P2P session and redirect.
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
@@ -85,24 +83,23 @@
|
|||||||
let challengeResult = await doChallenge(roomId);
|
let challengeResult = await doChallenge(roomId);
|
||||||
|
|
||||||
if (challengeResult) {
|
if (challengeResult) {
|
||||||
let unsubscribe = ws.handleEvent(
|
let unsubscribe = ws.handleEvent(WebSocketResponseType.ROOM_STATUS, (value) => {
|
||||||
WebSocketMessageType.ROOM_STATUS,
|
if (value.status === RoomStatusType.OPEN) {
|
||||||
(value) => {
|
unsubscribe();
|
||||||
if (value.status === "found") {
|
roomExists = true;
|
||||||
unsubscribe();
|
} else if (value.status === RoomStatusType.NOT_FOUND) {
|
||||||
roomExists = true;
|
unsubscribe();
|
||||||
} else if (value.status === "not-found") {
|
roomExists = false;
|
||||||
unsubscribe();
|
}
|
||||||
roomExists = false;
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.CHECK_ROOM_EXISTS,
|
type: WebSocketRequestType.ROOM_STATUS,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
nonce: challengeResult.nonce,
|
challenge: {
|
||||||
challenge: challengeResult.challenge,
|
target: challengeResult.target,
|
||||||
|
nonce: challengeResult.nonce,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,32 +112,28 @@
|
|||||||
Something went wrong: {$error.toLocaleLowerCase()}
|
Something went wrong: {$error.toLocaleLowerCase()}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="!text-paragraph">
|
<p class="!text-paragraph">
|
||||||
click <a href="/">here</a> to go back to the homepage
|
click <a href="/">here</a>
|
||||||
|
to go back to the homepage
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !$error}
|
{#if !$error}
|
||||||
{#if isHost}
|
{#if isHost}
|
||||||
{#if !$room.RTCConnectionReady}
|
{#if !$room.RTCConnectionReady}
|
||||||
<h2 class="text-3xl font-bold text-white mb-2">
|
<h2 class="text-3xl font-bold text-white mb-2">Your secure room is ready.</h2>
|
||||||
Your secure room is ready.
|
|
||||||
</h2>
|
|
||||||
<p class="text-gray-400 mb-6 text-center">
|
<p class="text-gray-400 mb-6 text-center">
|
||||||
Share the link below to invite someone to chat directly with
|
Share the link below to invite someone to chat directly with you. Once they
|
||||||
you. Once they join, you will be connected automatically.
|
join, you will be connected automatically.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-gray-900 rounded-lg p-4 flex items-center justify-between gap-4 border border-gray-600"
|
class="bg-gray-900 rounded-lg p-4 flex items-center justify-between gap-4 border border-gray-600">
|
||||||
>
|
<span class="text-accent font-mono text-sm overflow-x-auto whitespace-nowrap">
|
||||||
<span
|
{roomLink}
|
||||||
class="text-accent font-mono text-sm overflow-x-auto whitespace-nowrap"
|
</span>
|
||||||
>{roomLink}</span
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onclick={handleCopyLink}
|
onclick={handleCopyLink}
|
||||||
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
|
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap">
|
||||||
>
|
|
||||||
{copyButtonText}
|
{copyButtonText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,35 +143,31 @@
|
|||||||
{:else if awaitingJoinConfirmation}
|
{:else if awaitingJoinConfirmation}
|
||||||
{#if $ws.status !== WebsocketConnectionState.CONNECTED || roomExists === undefined}
|
{#if $ws.status !== WebsocketConnectionState.CONNECTED || roomExists === undefined}
|
||||||
<h2 class="text-3xl font-bold text-white mb-2">
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
<span class="flex items-center"
|
<span class="flex items-center">
|
||||||
><LoadingSpinner size="24" /> Connecting to server...</span
|
<LoadingSpinner size="24" /> Connecting to server...
|
||||||
>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="!text-paragraph">
|
<p class="!text-paragraph">
|
||||||
click <a href="/">here</a> to go back to the homepage
|
click <a href="/">here</a>
|
||||||
|
to go back to the homepage
|
||||||
</p>
|
</p>
|
||||||
{:else if roomExists === false}
|
{:else if roomExists === false}
|
||||||
<h2 class="text-3xl font-bold text-white mb-2">
|
<h2 class="text-3xl font-bold text-white mb-2">That room does not exist.</h2>
|
||||||
That room does not exist.
|
|
||||||
</h2>
|
|
||||||
<p class="!text-paragraph">
|
<p class="!text-paragraph">
|
||||||
click <a href="/">here</a> to go back to the homepage
|
click <a href="/">here</a>
|
||||||
|
to go back to the homepage
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<h2 class="text-3xl font-bold text-white mb-2">
|
<h2 class="text-3xl font-bold text-white mb-2">You're invited to chat.</h2>
|
||||||
You're invited to chat.
|
|
||||||
</h2>
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={handleConfirmJoin}
|
onclick={handleConfirmJoin}
|
||||||
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
|
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap">
|
||||||
>
|
|
||||||
Accept
|
Accept
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={handleDeclineJoin}
|
onclick={handleDeclineJoin}
|
||||||
class="bg-red-400 hover:bg-red-400/80 active:bg-red-400/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
|
class="bg-red-400 hover:bg-red-400/80 active:bg-red-400/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap">
|
||||||
>
|
|
||||||
Decline
|
Decline
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { Socket, WebSocketMessageType, type WebSocketMessage } from '$types/websocket';
|
import { Socket, type WebSocketMessage, type WebSocketMessageType } from '$types/websocket';
|
||||||
import { handleMessage } from '../lib/webrtcUtil';
|
import { handleMessage } from '../lib/webrtcUtil';
|
||||||
|
|
||||||
export enum WebsocketConnectionState {
|
export enum WebsocketConnectionState {
|
||||||
|
|||||||
@@ -11,143 +11,187 @@ export interface Room {
|
|||||||
connectionState: RoomConnectionState;
|
connectionState: RoomConnectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WebSocketMessageType {
|
export interface Challenge {
|
||||||
// room messages
|
// the answer is the sha256(additionalData + target + nonce)
|
||||||
CREATE_ROOM = "create",
|
target: string;
|
||||||
JOIN_ROOM = "join",
|
nonce: string;
|
||||||
LEAVE_ROOM = "leave",
|
}
|
||||||
CHECK_ROOM_EXISTS = "check",
|
|
||||||
REQUEST_CHALLENGE = "request-challenge",
|
|
||||||
|
|
||||||
// response messages
|
export enum WebSocketRequestType {
|
||||||
ROOM_CREATED = "created",
|
CREATE_ROOM = "create-room",
|
||||||
ROOM_JOINED = "joined",
|
ROOM_JOIN = "join-room",
|
||||||
ROOM_LEFT = "left",
|
ROOM_LEAVE = "leave-room",
|
||||||
ROOM_READY = "ready",
|
ROOM_STATUS = "get-room-status",
|
||||||
ROOM_STATUS = "status",
|
CHALLENGE_REQUEST = "get-challenge",
|
||||||
CHALLENGE = "challenge",
|
}
|
||||||
|
|
||||||
// webrtc messages
|
export enum WebSocketResponseType {
|
||||||
WEBRTC_OFFER = "offer",
|
ROOM_CREATED = "room-created",
|
||||||
WERTC_ANSWER = "answer",
|
ROOM_JOINED = "room-joined",
|
||||||
WEBRTC_ICE_CANDIDATE = "ice-candidate",
|
ROOM_LEFT = "room-left",
|
||||||
|
ROOM_STATUS = "room-status",
|
||||||
|
CHALLENGE_RESPONSE = "challenge",
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages sent to room participants providing information about the room
|
||||||
|
export enum WebSocketRoomMessageType {
|
||||||
|
PARTICIPANT_LEFT = "peer-left",
|
||||||
|
ROOM_READY = "room-ready",
|
||||||
|
PARTICIPANT_JOINED = "peer-joined",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WebSocketWebRtcMessageType {
|
||||||
|
OFFER = "offer",
|
||||||
|
ANSWER = "answer",
|
||||||
|
ICE_CANDIDATE = "ice-candidate",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WebSocketErrorType {
|
||||||
ERROR = "error",
|
ERROR = "error",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WebSocketMessageType = WebSocketRequestType | WebSocketResponseType | WebSocketRoomMessageType | WebSocketWebRtcMessageType | WebSocketErrorType;
|
||||||
|
|
||||||
// TODO: name the interfaces better
|
// TODO: name the interfaces better
|
||||||
export type WebSocketMessage =
|
export type WebSocketMessage =
|
||||||
| CreateRoomMessage
|
// request messages
|
||||||
| JoinRoomMessage
|
| CreateRoomRequest
|
||||||
| LeaveRoomMessage
|
| JoinRoomRequest
|
||||||
| CheckRoomExistsMessage
|
| LeaveRoomRequest
|
||||||
| RequestChallengeMessage
|
| RoomStatusRequest
|
||||||
| RoomCreatedMessage
|
| ChallengeRequest
|
||||||
| RoomJoinedMessage
|
// response messages
|
||||||
| RoomLeftMessage
|
| RoomCreatedResponse
|
||||||
| RoomStatusMessage
|
| RoomJoinedResponse
|
||||||
|
| RoomLeftResponse
|
||||||
|
| RoomStatusResponse
|
||||||
|
| ChallengeResponse
|
||||||
|
// room messages
|
||||||
|
| ParticipantJoinedMessage
|
||||||
|
| ParticipantLeftMessage
|
||||||
| RoomReadyMessage
|
| RoomReadyMessage
|
||||||
| ChallengeMessage
|
// webrtc messages
|
||||||
| OfferMessage
|
| WebRTCOfferMessage
|
||||||
| AnswerMessage
|
| WebRTCAnswerMessage
|
||||||
| IceCandidateMessage
|
| WebRTCIceCandidateMessage
|
||||||
| ErrorMessage;
|
// errors
|
||||||
|
| Error;
|
||||||
|
|
||||||
interface ErrorMessage {
|
interface Error {
|
||||||
type: WebSocketMessageType.ERROR;
|
type: WebSocketErrorType.ERROR;
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Query Messages ======
|
// ====== Query Messages ======
|
||||||
interface CreateRoomMessage {
|
interface CreateRoomRequest {
|
||||||
type: WebSocketMessageType.CREATE_ROOM;
|
type: WebSocketRequestType.CREATE_ROOM;
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
nonce: string;
|
challenge: Challenge;
|
||||||
challenge: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is used as a query message, but it's also used as a response message
|
interface JoinRoomRequest {
|
||||||
interface JoinRoomMessage {
|
type: WebSocketRequestType.ROOM_JOIN;
|
||||||
type: WebSocketMessageType.JOIN_ROOM;
|
|
||||||
roomId: string;
|
roomId: string;
|
||||||
nonce?: string;
|
challenge: Challenge;
|
||||||
challenge?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LeaveRoomMessage {
|
interface LeaveRoomRequest {
|
||||||
type: WebSocketMessageType.LEAVE_ROOM;
|
type: WebSocketRequestType.ROOM_LEAVE;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckRoomExistsMessage {
|
interface RoomStatusRequest {
|
||||||
type: WebSocketMessageType.CHECK_ROOM_EXISTS;
|
type: WebSocketRequestType.ROOM_STATUS;
|
||||||
// if sha256(roomId + challenge + nonce) has a certain number of leading zeros, then we can give the status to the user
|
|
||||||
roomId: string;
|
roomId: string;
|
||||||
nonce: string;
|
challenge: Challenge;
|
||||||
challenge: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestChallengeMessage {
|
interface ChallengeRequest {
|
||||||
type: WebSocketMessageType.REQUEST_CHALLENGE;
|
type: WebSocketRequestType.CHALLENGE_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Response Messages ======
|
// ====== Response Messages ======
|
||||||
|
interface RoomCreatedResponse {
|
||||||
interface RoomCreatedMessage {
|
type: WebSocketResponseType.ROOM_CREATED;
|
||||||
type: WebSocketMessageType.ROOM_CREATED;
|
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomJoinedMessage {
|
interface RoomJoinedResponse {
|
||||||
type: WebSocketMessageType.ROOM_JOINED;
|
type: WebSocketResponseType.ROOM_JOINED;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
participants: number;
|
participants: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomLeftMessage {
|
interface RoomLeftResponse {
|
||||||
type: WebSocketMessageType.ROOM_LEFT;
|
type: WebSocketResponseType.ROOM_LEFT;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomStatusMessage {
|
interface RoomStatusRequest {
|
||||||
type: WebSocketMessageType.ROOM_STATUS;
|
type: WebSocketRequestType.ROOM_STATUS;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
status: 'found' | 'not-found';
|
}
|
||||||
|
|
||||||
|
export enum RoomStatusType {
|
||||||
|
OPEN = "open",
|
||||||
|
FULL = "full",
|
||||||
|
NOT_FOUND = "not-found",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomStatusResponse {
|
||||||
|
type: WebSocketResponseType.ROOM_STATUS;
|
||||||
|
roomId: string;
|
||||||
|
status: RoomStatusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChallengeResponse {
|
||||||
|
type: WebSocketResponseType.CHALLENGE_RESPONSE;
|
||||||
|
target: string;
|
||||||
|
difficulty: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Room messages ======
|
||||||
|
interface ParticipantJoinedMessage {
|
||||||
|
type: WebSocketRoomMessageType.PARTICIPANT_JOINED;
|
||||||
|
roomId: string;
|
||||||
|
participants: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParticipantLeftMessage {
|
||||||
|
type: WebSocketRoomMessageType.PARTICIPANT_LEFT;
|
||||||
|
roomId: string;
|
||||||
|
participants: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomReadyMessage {
|
interface RoomReadyMessage {
|
||||||
type: WebSocketMessageType.ROOM_READY;
|
type: WebSocketRoomMessageType.ROOM_READY;
|
||||||
data: {
|
data: {
|
||||||
isInitiator: boolean;
|
isInitiator: boolean;
|
||||||
|
roomId: string;
|
||||||
|
participants: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChallengeMessage {
|
|
||||||
type: WebSocketMessageType.CHALLENGE;
|
|
||||||
challenge: string;
|
|
||||||
difficulty: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== WebRTC signaling messages ======
|
// ====== WebRTC signaling messages ======
|
||||||
// as the server, we dont do anything with these messages other than relay them to the other peers in the room
|
// as the server, we dont do anything with these messages other than relay them to the other peers in the room
|
||||||
interface OfferMessage {
|
interface WebRTCOfferMessage {
|
||||||
type: WebSocketMessageType.WEBRTC_OFFER;
|
type: WebSocketWebRtcMessageType.OFFER;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
sdp: RTCSessionDescriptionInit;
|
sdp: RTCSessionDescriptionInit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnswerMessage {
|
interface WebRTCAnswerMessage {
|
||||||
type: WebSocketMessageType.WERTC_ANSWER;
|
type: WebSocketWebRtcMessageType.ANSWER;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
sdp: RTCSessionDescriptionInit;
|
sdp: RTCSessionDescriptionInit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IceCandidateMessage {
|
interface WebRTCIceCandidateMessage {
|
||||||
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE;
|
type: WebSocketWebRtcMessageType.ICE_CANDIDATE;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
candidate: RTCIceCandidateInit;
|
candidate: RTCIceCandidateInit;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const config = {
|
|||||||
$stores: './src/stores',
|
$stores: './src/stores',
|
||||||
$components: './src/components',
|
$components: './src/components',
|
||||||
$types: './src/types',
|
$types: './src/types',
|
||||||
'$lib/server': './src/lib/server',
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user