u-foo 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/onlineCoreCommands.js +6 -0
- package/src/online/bridge.js +11 -1
- package/src/online/runner.js +41 -9
- package/src/online/server.js +154 -14
package/package.json
CHANGED
|
@@ -201,6 +201,8 @@ function runOnlineInbox(nickname, opts = {}) {
|
|
|
201
201
|
checkInbox(nickname, {
|
|
202
202
|
clear: !!opts.clear,
|
|
203
203
|
unread: !!opts.unread,
|
|
204
|
+
room: opts.room || "",
|
|
205
|
+
channel: opts.channel || "",
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -280,6 +282,8 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
280
282
|
return runOnlineInbox(payload.nickname, {
|
|
281
283
|
clear: opts.clear,
|
|
282
284
|
unread: opts.unread,
|
|
285
|
+
room: opts.room || "",
|
|
286
|
+
channel: opts.channel || "",
|
|
283
287
|
});
|
|
284
288
|
default:
|
|
285
289
|
throw createUnknownOnlineError(subcmd);
|
|
@@ -374,6 +378,8 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
374
378
|
return runOnlineInbox(argv[1], {
|
|
375
379
|
clear: hasFallbackFlag(argv, "--clear"),
|
|
376
380
|
unread: hasFallbackFlag(argv, "--unread"),
|
|
381
|
+
room: getFallbackOpt(argv, "--room"),
|
|
382
|
+
channel: getFallbackOpt(argv, "--channel"),
|
|
377
383
|
});
|
|
378
384
|
}
|
|
379
385
|
default:
|
package/src/online/bridge.js
CHANGED
|
@@ -135,6 +135,15 @@ function normalizeOnlineSender(from = "") {
|
|
|
135
135
|
return text;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
function inboxFileName(nickname, route) {
|
|
139
|
+
// Namespace inbox by room/channel to prevent cross-room message leakage
|
|
140
|
+
if (route) {
|
|
141
|
+
const safe = String(route).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
142
|
+
return `${nickname}__${safe}.jsonl`;
|
|
143
|
+
}
|
|
144
|
+
return `${nickname}.jsonl`;
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
function appendToInbox(nickname, msg) {
|
|
139
148
|
const dir = inboxDir();
|
|
140
149
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -143,7 +152,8 @@ function appendToInbox(nickname, msg) {
|
|
|
143
152
|
_source: messageSource(msg),
|
|
144
153
|
_receivedAt: new Date().toISOString(),
|
|
145
154
|
};
|
|
146
|
-
|
|
155
|
+
const route = msg.room || msg.channel || "";
|
|
156
|
+
fs.appendFileSync(path.join(dir, inboxFileName(nickname, route)), JSON.stringify(entry) + "\n");
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
// --- Helpers ---
|
package/src/online/runner.js
CHANGED
|
@@ -14,6 +14,28 @@ function inboxFilePath(nickname) {
|
|
|
14
14
|
return path.join(inboxDir(), `${nickname}.jsonl`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Find all inbox files for a given nickname (including room/channel-namespaced ones).
|
|
19
|
+
* Returns array of { file, route } objects.
|
|
20
|
+
*/
|
|
21
|
+
function findInboxFiles(nickname) {
|
|
22
|
+
const dir = inboxDir();
|
|
23
|
+
if (!fs.existsSync(dir)) return [];
|
|
24
|
+
const prefix = `${nickname}__`;
|
|
25
|
+
const exact = `${nickname}.jsonl`;
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const name of fs.readdirSync(dir)) {
|
|
28
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
29
|
+
if (name === exact) {
|
|
30
|
+
results.push({ file: path.join(dir, name), route: "" });
|
|
31
|
+
} else if (name.startsWith(prefix)) {
|
|
32
|
+
const route = name.slice(prefix.length, -".jsonl".length);
|
|
33
|
+
results.push({ file: path.join(dir, name), route });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
function readMarkerPath(nickname) {
|
|
18
40
|
return path.join(inboxDir(), `${nickname}.read`);
|
|
19
41
|
}
|
|
@@ -80,6 +102,8 @@ function cleanupInbox(file) {
|
|
|
80
102
|
function checkInbox(nickname, options = {}) {
|
|
81
103
|
const clear = options.clear || false;
|
|
82
104
|
const unreadOnly = options.unread || false;
|
|
105
|
+
const filterRoom = options.room || "";
|
|
106
|
+
const filterChannel = options.channel || "";
|
|
83
107
|
|
|
84
108
|
if (!nickname) {
|
|
85
109
|
console.error("nickname is required");
|
|
@@ -87,23 +111,24 @@ function checkInbox(nickname, options = {}) {
|
|
|
87
111
|
return;
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
const file = inboxFilePath(nickname);
|
|
91
114
|
const markerFile = readMarkerPath(nickname);
|
|
92
115
|
|
|
116
|
+
// Gather all inbox files for this nickname
|
|
117
|
+
const inboxFiles = findInboxFiles(nickname);
|
|
118
|
+
|
|
93
119
|
if (clear) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else {
|
|
98
|
-
console.log(`No inbox file for ${nickname}.`);
|
|
120
|
+
let cleared = 0;
|
|
121
|
+
for (const { file } of inboxFiles) {
|
|
122
|
+
if (fs.existsSync(file)) { fs.unlinkSync(file); cleared++; }
|
|
99
123
|
}
|
|
124
|
+
console.log(cleared > 0 ? `Inbox cleared for ${nickname} (${cleared} file(s)).` : `No inbox file for ${nickname}.`);
|
|
100
125
|
return;
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
cleanupInbox(file);
|
|
104
|
-
|
|
105
128
|
let messages = [];
|
|
106
|
-
|
|
129
|
+
for (const { file } of inboxFiles) {
|
|
130
|
+
cleanupInbox(file);
|
|
131
|
+
if (!fs.existsSync(file)) continue;
|
|
107
132
|
const lines = fs.readFileSync(file, "utf-8").split("\n").filter(Boolean);
|
|
108
133
|
for (const line of lines) {
|
|
109
134
|
try {
|
|
@@ -114,6 +139,13 @@ function checkInbox(nickname, options = {}) {
|
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
|
|
142
|
+
// Filter by room/channel if specified
|
|
143
|
+
if (filterRoom) {
|
|
144
|
+
messages = messages.filter((m) => m.room === filterRoom);
|
|
145
|
+
} else if (filterChannel) {
|
|
146
|
+
messages = messages.filter((m) => m.channel === filterChannel);
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
function displayWidth(str) {
|
|
118
150
|
let w = 0;
|
|
119
151
|
for (const ch of str) {
|
package/src/online/server.js
CHANGED
|
@@ -46,6 +46,7 @@ class OnlineServer extends EventEmitter {
|
|
|
46
46
|
this.rooms = new Map();
|
|
47
47
|
this.roomPasswords = new Map();
|
|
48
48
|
this.channelMessageHistory = new Map();
|
|
49
|
+
this.roomMessageHistory = new Map();
|
|
49
50
|
this.channelHistoryLimit = options.channelHistoryLimit ?? 200;
|
|
50
51
|
this.eventSeq = 0;
|
|
51
52
|
|
|
@@ -147,6 +148,7 @@ class OnlineServer extends EventEmitter {
|
|
|
147
148
|
const pathname = parsedUrl.pathname || "/";
|
|
148
149
|
const publicMessagesMatch = pathname.match(/^\/ufoo\/online\/public\/channels\/([^/]+)\/messages$/);
|
|
149
150
|
const privateMessagesMatch = pathname.match(/^\/ufoo\/online\/channels\/([^/]+)\/messages$/);
|
|
151
|
+
const roomMessagesMatch = pathname.match(/^\/ufoo\/online\/rooms\/([^/]+)\/messages$/);
|
|
150
152
|
|
|
151
153
|
if (req.method === "OPTIONS") {
|
|
152
154
|
res.writeHead(204, this.corsHeaders());
|
|
@@ -177,6 +179,20 @@ class OnlineServer extends EventEmitter {
|
|
|
177
179
|
return;
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
if (roomMessagesMatch) {
|
|
183
|
+
const roomId = decodeURIComponent(roomMessagesMatch[1] || "");
|
|
184
|
+
this.handleRoomMessagesRequest(req, res, roomId, parsedUrl);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// DELETE /ufoo/online/rooms/:id
|
|
189
|
+
const roomDeleteMatch = pathname.match(/^\/ufoo\/online\/rooms\/([^/]+)$/);
|
|
190
|
+
if (roomDeleteMatch && req.method === "DELETE") {
|
|
191
|
+
if (!this.authenticateHttp(req, res)) return;
|
|
192
|
+
this.handleRoomDelete(req, res, decodeURIComponent(roomDeleteMatch[1]));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
180
196
|
if (pathname.startsWith("/ufoo/online/rooms")) {
|
|
181
197
|
// Step 4: HTTP auth
|
|
182
198
|
if (!this.authenticateHttp(req, res)) return;
|
|
@@ -431,6 +447,39 @@ class OnlineServer extends EventEmitter {
|
|
|
431
447
|
});
|
|
432
448
|
}
|
|
433
449
|
|
|
450
|
+
handleRoomMessagesRequest(req, res, roomId, parsedUrl) {
|
|
451
|
+
if (req.method !== "GET") {
|
|
452
|
+
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const room = this.rooms.get(roomId);
|
|
456
|
+
if (!room) {
|
|
457
|
+
this.sendJson(res, 404, { ok: false, error: "Room not found" });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// Private rooms require password via query param
|
|
461
|
+
if (room.type === "private") {
|
|
462
|
+
const password = String(parsedUrl.searchParams.get("password") || "");
|
|
463
|
+
const stored = this.roomPasswords.get(roomId);
|
|
464
|
+
if (!stored || !this.verifyPassword(password, stored)) {
|
|
465
|
+
this.sendJson(res, 403, { ok: false, error: "Invalid room password" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const limitRaw = Number.parseInt(String(parsedUrl.searchParams.get("limit") || "80"), 10);
|
|
470
|
+
const limit = Number.isFinite(limitRaw) ? limitRaw : 80;
|
|
471
|
+
const roomData = this.listRoomMessages(roomId, limit);
|
|
472
|
+
if (!roomData) {
|
|
473
|
+
this.sendJson(res, 404, { ok: false, error: "Room not found" });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.sendJson(res, 200, {
|
|
477
|
+
ok: true,
|
|
478
|
+
room: { room_id: roomData.room_id, name: roomData.name, type: roomData.type },
|
|
479
|
+
messages: roomData.messages,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
434
483
|
recordChannelMessage(channelId, channel, client, eventPayload) {
|
|
435
484
|
const rawText = eventPayload?.payload?.message;
|
|
436
485
|
let text = "";
|
|
@@ -459,6 +508,45 @@ class OnlineServer extends EventEmitter {
|
|
|
459
508
|
}
|
|
460
509
|
}
|
|
461
510
|
|
|
511
|
+
recordRoomMessage(roomId, room, client, eventPayload) {
|
|
512
|
+
const rawText = eventPayload?.payload?.message;
|
|
513
|
+
let text = "";
|
|
514
|
+
if (typeof rawText === "string") text = rawText;
|
|
515
|
+
else if (rawText !== null && rawText !== undefined) text = JSON.stringify(rawText);
|
|
516
|
+
if (!text) return;
|
|
517
|
+
|
|
518
|
+
const history = this.roomMessageHistory.get(roomId) || [];
|
|
519
|
+
const ts = eventPayload.ts || new Date().toISOString();
|
|
520
|
+
const entry = {
|
|
521
|
+
event_id: `event_${String(++this.eventSeq).padStart(8, "0")}`,
|
|
522
|
+
ts,
|
|
523
|
+
room_id: roomId,
|
|
524
|
+
room_name: room?.name || roomId,
|
|
525
|
+
from: client.subscriberId || "",
|
|
526
|
+
nickname: client.nickname || "",
|
|
527
|
+
text,
|
|
528
|
+
};
|
|
529
|
+
history.push(entry);
|
|
530
|
+
if (history.length > this.channelHistoryLimit) {
|
|
531
|
+
history.splice(0, history.length - this.channelHistoryLimit);
|
|
532
|
+
}
|
|
533
|
+
this.roomMessageHistory.set(roomId, history);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
listRoomMessages(roomId, limit = 80) {
|
|
537
|
+
const room = this.rooms.get(roomId);
|
|
538
|
+
if (!room) return null;
|
|
539
|
+
const history = this.roomMessageHistory.get(roomId) || [];
|
|
540
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 80;
|
|
541
|
+
const start = Math.max(0, history.length - safeLimit);
|
|
542
|
+
return {
|
|
543
|
+
room_id: roomId,
|
|
544
|
+
name: room.name || "",
|
|
545
|
+
type: room.type,
|
|
546
|
+
messages: history.slice(start),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
462
550
|
handleRoomsRequest(req, res) {
|
|
463
551
|
if (req.method === "GET") {
|
|
464
552
|
this.sendJson(res, 200, { ok: true, rooms: this.listRooms() });
|
|
@@ -518,6 +606,7 @@ class OnlineServer extends EventEmitter {
|
|
|
518
606
|
name,
|
|
519
607
|
type,
|
|
520
608
|
members: new Set(),
|
|
609
|
+
observers: new Set(),
|
|
521
610
|
created_at: new Date().toISOString(),
|
|
522
611
|
created_by: createdBy,
|
|
523
612
|
});
|
|
@@ -536,6 +625,25 @@ class OnlineServer extends EventEmitter {
|
|
|
536
625
|
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
537
626
|
}
|
|
538
627
|
|
|
628
|
+
handleRoomDelete(req, res, roomId) {
|
|
629
|
+
const room = this.rooms.get(roomId);
|
|
630
|
+
if (!room) {
|
|
631
|
+
this.sendJson(res, 404, { ok: false, error: "Room not found" });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Disconnect all members and observers
|
|
635
|
+
const kick = (client) => {
|
|
636
|
+
client.rooms.delete(roomId);
|
|
637
|
+
this.sendError(client.ws, "Room deleted", true, "ROOM_DELETED");
|
|
638
|
+
};
|
|
639
|
+
room.members.forEach(kick);
|
|
640
|
+
if (room.observers) room.observers.forEach(kick);
|
|
641
|
+
this.rooms.delete(roomId);
|
|
642
|
+
this.roomPasswords.delete(roomId);
|
|
643
|
+
this.roomMessageHistory.delete(roomId);
|
|
644
|
+
this.sendJson(res, 200, { ok: true, deleted: roomId });
|
|
645
|
+
}
|
|
646
|
+
|
|
539
647
|
handleChannelsRequest(req, res) {
|
|
540
648
|
if (req.method === "GET") {
|
|
541
649
|
this.sendJson(res, 200, { ok: true, channels: this.listChannels() });
|
|
@@ -593,6 +701,7 @@ class OnlineServer extends EventEmitter {
|
|
|
593
701
|
name,
|
|
594
702
|
type,
|
|
595
703
|
members: new Set(),
|
|
704
|
+
observers: new Set(),
|
|
596
705
|
created_at: new Date().toISOString(),
|
|
597
706
|
created_by: createdBy,
|
|
598
707
|
});
|
|
@@ -650,6 +759,7 @@ class OnlineServer extends EventEmitter {
|
|
|
650
759
|
authed: false,
|
|
651
760
|
subscriberId: null,
|
|
652
761
|
nickname: null,
|
|
762
|
+
role: "agent", // "agent" or "observer"
|
|
653
763
|
channels: new Set(),
|
|
654
764
|
helloReceived: false,
|
|
655
765
|
connectedAt: Date.now(),
|
|
@@ -796,6 +906,12 @@ class OnlineServer extends EventEmitter {
|
|
|
796
906
|
client.pendingWorld = world;
|
|
797
907
|
client.rooms = new Set();
|
|
798
908
|
|
|
909
|
+
// Observer role: set from capabilities
|
|
910
|
+
const caps = Array.isArray(info.capabilities) ? info.capabilities : [];
|
|
911
|
+
if (caps.includes("observer")) {
|
|
912
|
+
client.role = "observer";
|
|
913
|
+
}
|
|
914
|
+
|
|
799
915
|
this.send(client.ws, {
|
|
800
916
|
type: "hello_ack",
|
|
801
917
|
ok: true,
|
|
@@ -902,6 +1018,7 @@ class OnlineServer extends EventEmitter {
|
|
|
902
1018
|
name: channelRef,
|
|
903
1019
|
type: "public",
|
|
904
1020
|
members: new Set(),
|
|
1021
|
+
observers: new Set(),
|
|
905
1022
|
created_at: new Date().toISOString(),
|
|
906
1023
|
created_by: "",
|
|
907
1024
|
};
|
|
@@ -938,7 +1055,12 @@ class OnlineServer extends EventEmitter {
|
|
|
938
1055
|
const channelId = resolved.channelId;
|
|
939
1056
|
const channelInfo = resolved.channel;
|
|
940
1057
|
|
|
941
|
-
|
|
1058
|
+
if (client.role === "observer") {
|
|
1059
|
+
if (!channelInfo.observers) channelInfo.observers = new Set();
|
|
1060
|
+
channelInfo.observers.add(client);
|
|
1061
|
+
} else {
|
|
1062
|
+
channelInfo.members.add(client);
|
|
1063
|
+
}
|
|
942
1064
|
client.channels.add(channelId);
|
|
943
1065
|
this.send(client.ws, { type: "join_ack", ok: true, channel: channelId });
|
|
944
1066
|
}
|
|
@@ -963,6 +1085,7 @@ class OnlineServer extends EventEmitter {
|
|
|
963
1085
|
const channelInfo = resolved?.channel || null;
|
|
964
1086
|
if (channelInfo) {
|
|
965
1087
|
channelInfo.members.delete(client);
|
|
1088
|
+
if (channelInfo.observers) channelInfo.observers.delete(client);
|
|
966
1089
|
}
|
|
967
1090
|
client.channels.delete(channelId);
|
|
968
1091
|
this.send(client.ws, { type: "leave_ack", ok: true, channel: channelId });
|
|
@@ -970,6 +1093,10 @@ class OnlineServer extends EventEmitter {
|
|
|
970
1093
|
|
|
971
1094
|
handleEvent(client, message) {
|
|
972
1095
|
if (!this.requireAuth(client)) return;
|
|
1096
|
+
if (client.role === "observer") {
|
|
1097
|
+
this.sendError(client.ws, "Observers are read-only", false, "EVENT_OBSERVER_READONLY");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
973
1100
|
if (!client.subscriberId) {
|
|
974
1101
|
this.sendError(client.ws, "Unknown subscriber", false, "SUBSCRIBER_UNKNOWN");
|
|
975
1102
|
return;
|
|
@@ -1026,14 +1153,19 @@ class OnlineServer extends EventEmitter {
|
|
|
1026
1153
|
this.sendError(client.ws, "Room not found", false, "ROOM_NOT_FOUND");
|
|
1027
1154
|
return;
|
|
1028
1155
|
}
|
|
1029
|
-
|
|
1030
|
-
if (
|
|
1031
|
-
this.send(
|
|
1156
|
+
const broadcastRoom = (recipient) => {
|
|
1157
|
+
if (recipient !== client) {
|
|
1158
|
+
this.send(recipient.ws, payload);
|
|
1032
1159
|
if (payload.payload && payload.payload.kind === "wake") {
|
|
1033
|
-
this.send(
|
|
1160
|
+
this.send(recipient.ws, { type: "wake", from: client.subscriberId });
|
|
1034
1161
|
}
|
|
1035
1162
|
}
|
|
1036
|
-
}
|
|
1163
|
+
};
|
|
1164
|
+
room.members.forEach(broadcastRoom);
|
|
1165
|
+
if (room.observers) room.observers.forEach(broadcastRoom);
|
|
1166
|
+
if (kind === "message") {
|
|
1167
|
+
this.recordRoomMessage(payload.room, room, client, payload);
|
|
1168
|
+
}
|
|
1037
1169
|
return;
|
|
1038
1170
|
}
|
|
1039
1171
|
|
|
@@ -1050,20 +1182,20 @@ class OnlineServer extends EventEmitter {
|
|
|
1050
1182
|
this.sendError(client.ws, "Join channel first", false, "NOT_IN_CHANNEL");
|
|
1051
1183
|
return;
|
|
1052
1184
|
}
|
|
1053
|
-
const members = channel ? channel.members : null;
|
|
1054
|
-
if (!members || members.size === 0) return;
|
|
1055
1185
|
payload.channel = channelId;
|
|
1056
1186
|
if (kind === "message") {
|
|
1057
1187
|
this.recordChannelMessage(channelId, channel, client, payload);
|
|
1058
1188
|
}
|
|
1059
|
-
|
|
1060
|
-
if (
|
|
1061
|
-
this.send(
|
|
1189
|
+
const broadcastChannel = (recipient) => {
|
|
1190
|
+
if (recipient !== client) {
|
|
1191
|
+
this.send(recipient.ws, payload);
|
|
1062
1192
|
if (payload.payload && payload.payload.kind === "wake") {
|
|
1063
|
-
this.send(
|
|
1193
|
+
this.send(recipient.ws, { type: "wake", from: client.subscriberId });
|
|
1064
1194
|
}
|
|
1065
1195
|
}
|
|
1066
|
-
}
|
|
1196
|
+
};
|
|
1197
|
+
if (channel.members) channel.members.forEach(broadcastChannel);
|
|
1198
|
+
if (channel.observers) channel.observers.forEach(broadcastChannel);
|
|
1067
1199
|
return;
|
|
1068
1200
|
}
|
|
1069
1201
|
|
|
@@ -1113,7 +1245,12 @@ class OnlineServer extends EventEmitter {
|
|
|
1113
1245
|
return;
|
|
1114
1246
|
}
|
|
1115
1247
|
|
|
1116
|
-
|
|
1248
|
+
if (client.role === "observer") {
|
|
1249
|
+
if (!room.observers) room.observers = new Set();
|
|
1250
|
+
room.observers.add(client);
|
|
1251
|
+
} else {
|
|
1252
|
+
room.members.add(client);
|
|
1253
|
+
}
|
|
1117
1254
|
client.rooms.add(roomId);
|
|
1118
1255
|
this.send(client.ws, { type: "join_ack", ok: true, room: roomId });
|
|
1119
1256
|
}
|
|
@@ -1127,6 +1264,7 @@ class OnlineServer extends EventEmitter {
|
|
|
1127
1264
|
const room = this.rooms.get(roomId);
|
|
1128
1265
|
if (room) {
|
|
1129
1266
|
room.members.delete(client);
|
|
1267
|
+
if (room.observers) room.observers.delete(client);
|
|
1130
1268
|
}
|
|
1131
1269
|
client.rooms.delete(roomId);
|
|
1132
1270
|
this.send(client.ws, { type: "leave_ack", ok: true, room: roomId });
|
|
@@ -1144,6 +1282,7 @@ class OnlineServer extends EventEmitter {
|
|
|
1144
1282
|
const channelInfo = this.channels.get(channel);
|
|
1145
1283
|
if (channelInfo) {
|
|
1146
1284
|
channelInfo.members.delete(client);
|
|
1285
|
+
if (channelInfo.observers) channelInfo.observers.delete(client);
|
|
1147
1286
|
}
|
|
1148
1287
|
});
|
|
1149
1288
|
client.channels.clear();
|
|
@@ -1153,6 +1292,7 @@ class OnlineServer extends EventEmitter {
|
|
|
1153
1292
|
const room = this.rooms.get(roomId);
|
|
1154
1293
|
if (room) {
|
|
1155
1294
|
room.members.delete(client);
|
|
1295
|
+
if (room.observers) room.observers.delete(client);
|
|
1156
1296
|
}
|
|
1157
1297
|
});
|
|
1158
1298
|
client.rooms.clear();
|