Skip to content

Commit 2bf3de9

Browse files
committed
chore: wss server handler has been refactored.
1 parent 0c4120e commit 2bf3de9

File tree

10 files changed

+398
-12
lines changed

10 files changed

+398
-12
lines changed

services/server/src/wss/handlers/call.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { canJoinCall } from "@/lib/call";
33
import { isGhost } from "@litespace/auth";
44
import { logger, safe } from "@litespace/sol";
55
import { ICall, Wss } from "@litespace/types";
6+
import { WSSHandler } from "@/wss/handlers/base";
7+
68
import zod from "zod";
7-
import { WSSHandler } from "../handler";
9+
import { asCallRoomId } from "../utils";
810

911
const callTypes = ["lesson", "interview"] as const satisfies ICall.Type[];
1012
const stdout = logger("wss");
@@ -25,6 +27,7 @@ export class CallHandler extends WSSHandler {
2527

2628
const user = this.user;
2729
// todo: add ghost as a member of the call
30+
if (!user) return stdout.error("user undefined!");
2831
if (isGhost(user)) return stdout.warning("Unsupported");
2932

3033
stdout.info(`User ${user.id} is joining call ${callId}.`);
@@ -39,7 +42,7 @@ export class CallHandler extends WSSHandler {
3942

4043
// add user to the call by inserting row to call_members relation
4144
await cache.call.addMember({ userId: user.id, callId: callId });
42-
this.socket.join(this.asCallRoomId(callId));
45+
this.socket.join(asCallRoomId(callId));
4346

4447
stdout.info(`User ${user.id} has joined call ${callId}.`);
4548

@@ -51,7 +54,7 @@ export class CallHandler extends WSSHandler {
5154
// NOTE: the user notifies himself as well that he successfully joined the call.
5255
this.broadcast(
5356
Wss.ServerEvent.MemberJoinedCall,
54-
this.asCallRoomId(callId),
57+
asCallRoomId(callId),
5558
{ userId: user.id } // TODO: define the payload struct type in the types package
5659
);
5760
});
@@ -82,16 +85,12 @@ export class CallHandler extends WSSHandler {
8285

8386
// notify members that a member has left the call
8487
this.socket.broadcast
85-
.to(this.asCallRoomId(callId))
88+
.to(asCallRoomId(callId))
8689
.emit(Wss.ServerEvent.MemberLeftCall, {
8790
userId: user.id,
8891
});
8992
});
9093
if (result instanceof Error) stdout.error(result.message);
9194
}
92-
93-
private asCallRoomId(callId: number) {
94-
return `call:${callId}`;
95-
}
9695
}
9796

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { isAdmin, isGhost, isStudent, isTutor } from "@litespace/auth";
2+
import { dayjs, logger, safe } from "@litespace/sol";
3+
import { IUser, Wss } from "@litespace/types";
4+
import { WSSHandler } from "@/wss/handlers/base";
5+
import { calls, rooms, users } from "@litespace/models";
6+
import { background } from "@/workers";
7+
import { PartentPortMessage, PartentPortMessageType } from "@/workers/messages";
8+
import { asCallRoomId, asChatRoomId } from "@/wss/utils";
9+
import { cache } from "@/lib/cache";
10+
import { getGhostCall } from "@litespace/sol/ghost";
11+
12+
const stdout = logger("wss");
13+
14+
export class ConnectionHandler extends WSSHandler {
15+
async connect() {
16+
const error = safe(async () => {
17+
const user = this.user;
18+
if (isGhost(user)) return;
19+
20+
const info = await users.update(user.id, { online: true });
21+
this.announceStatus(info);
22+
23+
await this.joinRooms();
24+
if (isAdmin(this.user)) this.emitServerStats();
25+
});
26+
if (error instanceof Error) stdout.error(error.message);
27+
}
28+
29+
async disconnect() {
30+
const error = safe(async () => {
31+
const user = this.user;
32+
if (isGhost(user)) return;
33+
34+
const info = await users.update(user.id, { online: false });
35+
this.announceStatus(info);
36+
37+
await this.deregisterPeer();
38+
await this.removeUserFromCalls();
39+
});
40+
if (error instanceof Error) stdout.error(error.message);
41+
}
42+
43+
private async announceStatus(user: IUser.Self) {
44+
const userRooms = await rooms.findMemberFullRoomIds(user.id);
45+
46+
for (const room of userRooms) {
47+
this.broadcast(Wss.ServerEvent.UserStatusChanged, room.toString(), {
48+
online: user.online,
49+
});
50+
}
51+
}
52+
53+
private async emitServerStats() {
54+
background.on("message", (message: PartentPortMessage) => {
55+
if (message.type === PartentPortMessageType.Stats)
56+
return this.socket.emit(Wss.ServerEvent.ServerStats, message.stats);
57+
});
58+
}
59+
60+
private async joinRooms() {
61+
const error = await safe(async () => {
62+
const user = this.user;
63+
if (isGhost(user)) return;
64+
65+
const { list } = await rooms.findMemberRooms({ userId: user.id });
66+
67+
this.socket.join(list.map((roomId) => asChatRoomId(roomId)));
68+
// private channel
69+
this.socket.join(user.id.toString());
70+
71+
if (isStudent(this.user)) this.socket.join(Wss.Room.TutorsCache);
72+
if (isAdmin(this.user)) this.socket.join(Wss.Room.ServerStats);
73+
74+
// todo: get user calls from cache
75+
const callsList = await calls.find({
76+
users: [user.id],
77+
full: true,
78+
after: dayjs.utc().startOf("day").toISOString(),
79+
before: dayjs.utc().add(1, "day").toISOString(),
80+
});
81+
82+
this.socket.join(
83+
callsList.list.map((call) => asCallRoomId(call.id))
84+
);
85+
});
86+
87+
if (error instanceof Error) stdout.error(error.message);
88+
}
89+
90+
/**
91+
* Remove ghost and tutor peer id from the cache.
92+
*
93+
* @note should be called when the socket disconnects from the server.
94+
*/
95+
private async deregisterPeer() {
96+
// todo: notify peers that the current user got disconnected
97+
const user = this.user;
98+
const display = isGhost(user) ? user : user.email;
99+
stdout.info(`Deregister peer for: ${display}`);
100+
101+
if (isGhost(user))
102+
return await cache.peer.removeGhostPeerId(getGhostCall(user));
103+
if (isTutor(user)) await cache.peer.removeUserPeerId(user.id);
104+
}
105+
106+
private async removeUserFromCalls() {
107+
const user = this.user;
108+
if (isGhost(user)) return;
109+
110+
const callId = await cache.call.removeMemberByUserId(user.id);
111+
if (!callId) return;
112+
113+
// notify members that a member has left the call
114+
this.socket.broadcast
115+
.to(asCallRoomId(callId))
116+
.emit(Wss.ServerEvent.MemberLeftCall, {
117+
userId: user.id,
118+
});
119+
}
120+
}
121+
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { Socket } from "socket.io";
2-
import { CallHandler } from "./call";
32
import { IUser } from "@litespace/types";
43

4+
import { CallHandler } from "./call";
5+
import { ConnectionHandler } from "./connection";
6+
import { MessageHandler } from "./message";
7+
import { PeerHandler } from "./peer";
8+
import { InputDevicesHandler } from "./inputDevices";
9+
510
export class WSSHandlers {
11+
public readonly connection: ConnectionHandler;
612
public readonly call: CallHandler;
13+
public readonly message: MessageHandler;
14+
public readonly peer: PeerHandler;
15+
public readonly inputDevices: InputDevicesHandler;
716

817
constructor(socket: Socket, user: IUser.Self | IUser.Ghost) {
18+
this.connection = new ConnectionHandler(socket, user);
919
this.call = new CallHandler(socket, user);
20+
this.message = new MessageHandler(socket, user);
21+
this.peer = new PeerHandler(socket, user);
22+
this.inputDevices = new InputDevicesHandler(socket, user);
1023
}
1124
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { logger, safe } from "@litespace/sol";
2+
import { rooms } from "@litespace/models";
3+
import { isGhost } from "@litespace/auth";
4+
import { Wss } from "@litespace/types";
5+
import { id, boolean } from "@/validation/utils";
6+
import { WSSHandler } from "./base";
7+
8+
import zod from "zod";
9+
import { isEmpty } from "lodash";
10+
11+
const toggleCameraPayload = zod.object({ call: id, camera: boolean });
12+
const toggleMicPayload = zod.object({ call: id, mic: boolean });
13+
const userTypingPayload = zod.object({ roomId: zod.number() });
14+
15+
const stdout = logger("wss");
16+
17+
export class InputDevicesHandler extends WSSHandler {
18+
async toggleCamera(data: unknown) {
19+
const error = safe(async () => {
20+
const user = this.user;
21+
if (isGhost(user)) return;
22+
const { call, camera } = toggleCameraPayload.parse(data);
23+
// todo: add validation
24+
this.broadcast(Wss.ServerEvent.CameraToggled, call.toString(), {
25+
user: user.id,
26+
camera,
27+
});
28+
});
29+
if (error instanceof Error) stdout.error(error.message);
30+
}
31+
32+
async toggleMic(data: unknown) {
33+
const error = safe(async () => {
34+
const user = this.user;
35+
if (isGhost(user)) return;
36+
const { call, mic } = toggleMicPayload.parse(data);
37+
// todo: add validation
38+
this.broadcast(Wss.ServerEvent.MicToggled, call.toString(), {
39+
user: user.id,
40+
mic,
41+
});
42+
});
43+
if (error instanceof Error) stdout.error(error.message);
44+
}
45+
46+
async userTyping(data: unknown) {
47+
const error = safe(async () => {
48+
const { roomId } = userTypingPayload.parse(data);
49+
50+
const user = this.user;
51+
if (isGhost(user)) return;
52+
53+
const members = await rooms.findRoomMembers({ roomIds: [roomId] });
54+
if (isEmpty(members)) return;
55+
56+
const isMember = members.find((member) => member.id === user.id);
57+
if (!isMember)
58+
throw new Error(`User(${user.id}) isn't member of room Id: ${roomId}`);
59+
60+
this.socket.to(roomId.toString()).emit(Wss.ServerEvent.UserTyping, {
61+
roomId,
62+
userId: user.id,
63+
});
64+
});
65+
66+
if (error instanceof Error) stdout.error(error.message);
67+
}
68+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { isGhost } from "@litespace/auth";
2+
import { logger, safe, sanitizeMessage } from "@litespace/sol";
3+
import { Wss } from "@litespace/types";
4+
import { WSSHandler } from "@/wss/handlers/base";
5+
import { messages, rooms } from "@litespace/models";
6+
import { asChatRoomId } from "@/wss/utils";
7+
import wss from "@/validation/wss";
8+
9+
import zod from "zod";
10+
import { id, string, withNamedId } from "@/validation/utils";
11+
12+
const stdout = logger("wss");
13+
14+
const updateMessagePayload = zod.object({ text: string, id });
15+
16+
export class MessageHandler extends WSSHandler {
17+
async sendMessage(data: unknown) {
18+
const error = safe(async () => {
19+
const user = this.user;
20+
if (isGhost(user)) return;
21+
22+
const { roomId, text } = wss.message.send.parse(data);
23+
const userId = user.id;
24+
25+
stdout.log(`u:${userId} is send a message to r:${roomId}`);
26+
27+
const members = await rooms.findRoomMembers({ roomIds: [roomId] });
28+
if (!members) throw Error("Room not found");
29+
30+
const member = members.map((member) => member.id).includes(userId);
31+
if (!member) throw new Error("Unauthorized");
32+
33+
const sanitized = sanitizeMessage(text);
34+
if (!sanitized) return; // empty message
35+
const message = await messages.create({
36+
text: sanitized,
37+
userId,
38+
roomId,
39+
});
40+
41+
this.broadcast(
42+
Wss.ServerEvent.RoomMessage,
43+
asChatRoomId(roomId),
44+
message
45+
);
46+
});
47+
if (error instanceof Error) stdout.error(error);
48+
}
49+
50+
async updateMessage(data: unknown) {
51+
const error = await safe(async () => {
52+
const user = this.user;
53+
if (isGhost(user)) return;
54+
55+
const { id, text } = updateMessagePayload.parse(data);
56+
const message = await messages.findById(id);
57+
if (!message || message.deleted) throw new Error("Message not found");
58+
59+
const owner = message.userId === user.id;
60+
if (!owner) throw new Error("Forbidden");
61+
62+
const sanitized = sanitizeMessage(text);
63+
if (!sanitized) throw new Error("Invalid message");
64+
65+
const updated = await messages.update(id, { text: sanitized });
66+
if (!updated) throw new Error("Mesasge not update; should never happen.");
67+
68+
this.broadcast(
69+
Wss.ServerEvent.RoomMessageUpdated,
70+
asChatRoomId(message.roomId),
71+
updated
72+
);
73+
});
74+
if (error instanceof Error) stdout.error(error.message);
75+
}
76+
77+
async deleteMessage(data: unknown) {
78+
const error = safe(async () => {
79+
const user = this.user;
80+
if (isGhost(user)) return;
81+
82+
const { id }: { id: number } = withNamedId("id").parse(data);
83+
84+
const message = await messages.findById(id);
85+
if (!message || message.deleted) throw new Error("Message not found");
86+
87+
const owner = message.userId === user.id;
88+
if (!owner) throw new Error("Forbidden");
89+
90+
await messages.markAsDeleted(id);
91+
92+
this.broadcast(
93+
Wss.ServerEvent.RoomMessageDeleted,
94+
asChatRoomId(message.roomId),
95+
{
96+
roomId: message.roomId,
97+
messageId: message.id,
98+
}
99+
);
100+
});
101+
if (error instanceof Error) stdout.error(error.message);
102+
}
103+
}

0 commit comments

Comments
 (0)