u-foo 1.3.0 → 1.4.0
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/online/server.js +127 -14
package/package.json
CHANGED
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,12 @@ 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
|
+
|
|
180
188
|
if (pathname.startsWith("/ufoo/online/rooms")) {
|
|
181
189
|
// Step 4: HTTP auth
|
|
182
190
|
if (!this.authenticateHttp(req, res)) return;
|
|
@@ -431,6 +439,39 @@ class OnlineServer extends EventEmitter {
|
|
|
431
439
|
});
|
|
432
440
|
}
|
|
433
441
|
|
|
442
|
+
handleRoomMessagesRequest(req, res, roomId, parsedUrl) {
|
|
443
|
+
if (req.method !== "GET") {
|
|
444
|
+
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const room = this.rooms.get(roomId);
|
|
448
|
+
if (!room) {
|
|
449
|
+
this.sendJson(res, 404, { ok: false, error: "Room not found" });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Private rooms require password via query param
|
|
453
|
+
if (room.type === "private") {
|
|
454
|
+
const password = String(parsedUrl.searchParams.get("password") || "");
|
|
455
|
+
const stored = this.roomPasswords.get(roomId);
|
|
456
|
+
if (!stored || !this.verifyPassword(password, stored)) {
|
|
457
|
+
this.sendJson(res, 403, { ok: false, error: "Invalid room password" });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const limitRaw = Number.parseInt(String(parsedUrl.searchParams.get("limit") || "80"), 10);
|
|
462
|
+
const limit = Number.isFinite(limitRaw) ? limitRaw : 80;
|
|
463
|
+
const roomData = this.listRoomMessages(roomId, limit);
|
|
464
|
+
if (!roomData) {
|
|
465
|
+
this.sendJson(res, 404, { ok: false, error: "Room not found" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.sendJson(res, 200, {
|
|
469
|
+
ok: true,
|
|
470
|
+
room: { room_id: roomData.room_id, name: roomData.name, type: roomData.type },
|
|
471
|
+
messages: roomData.messages,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
434
475
|
recordChannelMessage(channelId, channel, client, eventPayload) {
|
|
435
476
|
const rawText = eventPayload?.payload?.message;
|
|
436
477
|
let text = "";
|
|
@@ -459,6 +500,45 @@ class OnlineServer extends EventEmitter {
|
|
|
459
500
|
}
|
|
460
501
|
}
|
|
461
502
|
|
|
503
|
+
recordRoomMessage(roomId, room, client, eventPayload) {
|
|
504
|
+
const rawText = eventPayload?.payload?.message;
|
|
505
|
+
let text = "";
|
|
506
|
+
if (typeof rawText === "string") text = rawText;
|
|
507
|
+
else if (rawText !== null && rawText !== undefined) text = JSON.stringify(rawText);
|
|
508
|
+
if (!text) return;
|
|
509
|
+
|
|
510
|
+
const history = this.roomMessageHistory.get(roomId) || [];
|
|
511
|
+
const ts = eventPayload.ts || new Date().toISOString();
|
|
512
|
+
const entry = {
|
|
513
|
+
event_id: `event_${String(++this.eventSeq).padStart(8, "0")}`,
|
|
514
|
+
ts,
|
|
515
|
+
room_id: roomId,
|
|
516
|
+
room_name: room?.name || roomId,
|
|
517
|
+
from: client.subscriberId || "",
|
|
518
|
+
nickname: client.nickname || "",
|
|
519
|
+
text,
|
|
520
|
+
};
|
|
521
|
+
history.push(entry);
|
|
522
|
+
if (history.length > this.channelHistoryLimit) {
|
|
523
|
+
history.splice(0, history.length - this.channelHistoryLimit);
|
|
524
|
+
}
|
|
525
|
+
this.roomMessageHistory.set(roomId, history);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
listRoomMessages(roomId, limit = 80) {
|
|
529
|
+
const room = this.rooms.get(roomId);
|
|
530
|
+
if (!room) return null;
|
|
531
|
+
const history = this.roomMessageHistory.get(roomId) || [];
|
|
532
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 80;
|
|
533
|
+
const start = Math.max(0, history.length - safeLimit);
|
|
534
|
+
return {
|
|
535
|
+
room_id: roomId,
|
|
536
|
+
name: room.name || "",
|
|
537
|
+
type: room.type,
|
|
538
|
+
messages: history.slice(start),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
462
542
|
handleRoomsRequest(req, res) {
|
|
463
543
|
if (req.method === "GET") {
|
|
464
544
|
this.sendJson(res, 200, { ok: true, rooms: this.listRooms() });
|
|
@@ -518,6 +598,7 @@ class OnlineServer extends EventEmitter {
|
|
|
518
598
|
name,
|
|
519
599
|
type,
|
|
520
600
|
members: new Set(),
|
|
601
|
+
observers: new Set(),
|
|
521
602
|
created_at: new Date().toISOString(),
|
|
522
603
|
created_by: createdBy,
|
|
523
604
|
});
|
|
@@ -593,6 +674,7 @@ class OnlineServer extends EventEmitter {
|
|
|
593
674
|
name,
|
|
594
675
|
type,
|
|
595
676
|
members: new Set(),
|
|
677
|
+
observers: new Set(),
|
|
596
678
|
created_at: new Date().toISOString(),
|
|
597
679
|
created_by: createdBy,
|
|
598
680
|
});
|
|
@@ -650,6 +732,7 @@ class OnlineServer extends EventEmitter {
|
|
|
650
732
|
authed: false,
|
|
651
733
|
subscriberId: null,
|
|
652
734
|
nickname: null,
|
|
735
|
+
role: "agent", // "agent" or "observer"
|
|
653
736
|
channels: new Set(),
|
|
654
737
|
helloReceived: false,
|
|
655
738
|
connectedAt: Date.now(),
|
|
@@ -796,6 +879,12 @@ class OnlineServer extends EventEmitter {
|
|
|
796
879
|
client.pendingWorld = world;
|
|
797
880
|
client.rooms = new Set();
|
|
798
881
|
|
|
882
|
+
// Observer role: set from capabilities
|
|
883
|
+
const caps = Array.isArray(info.capabilities) ? info.capabilities : [];
|
|
884
|
+
if (caps.includes("observer")) {
|
|
885
|
+
client.role = "observer";
|
|
886
|
+
}
|
|
887
|
+
|
|
799
888
|
this.send(client.ws, {
|
|
800
889
|
type: "hello_ack",
|
|
801
890
|
ok: true,
|
|
@@ -902,6 +991,7 @@ class OnlineServer extends EventEmitter {
|
|
|
902
991
|
name: channelRef,
|
|
903
992
|
type: "public",
|
|
904
993
|
members: new Set(),
|
|
994
|
+
observers: new Set(),
|
|
905
995
|
created_at: new Date().toISOString(),
|
|
906
996
|
created_by: "",
|
|
907
997
|
};
|
|
@@ -938,7 +1028,12 @@ class OnlineServer extends EventEmitter {
|
|
|
938
1028
|
const channelId = resolved.channelId;
|
|
939
1029
|
const channelInfo = resolved.channel;
|
|
940
1030
|
|
|
941
|
-
|
|
1031
|
+
if (client.role === "observer") {
|
|
1032
|
+
if (!channelInfo.observers) channelInfo.observers = new Set();
|
|
1033
|
+
channelInfo.observers.add(client);
|
|
1034
|
+
} else {
|
|
1035
|
+
channelInfo.members.add(client);
|
|
1036
|
+
}
|
|
942
1037
|
client.channels.add(channelId);
|
|
943
1038
|
this.send(client.ws, { type: "join_ack", ok: true, channel: channelId });
|
|
944
1039
|
}
|
|
@@ -963,6 +1058,7 @@ class OnlineServer extends EventEmitter {
|
|
|
963
1058
|
const channelInfo = resolved?.channel || null;
|
|
964
1059
|
if (channelInfo) {
|
|
965
1060
|
channelInfo.members.delete(client);
|
|
1061
|
+
if (channelInfo.observers) channelInfo.observers.delete(client);
|
|
966
1062
|
}
|
|
967
1063
|
client.channels.delete(channelId);
|
|
968
1064
|
this.send(client.ws, { type: "leave_ack", ok: true, channel: channelId });
|
|
@@ -970,6 +1066,10 @@ class OnlineServer extends EventEmitter {
|
|
|
970
1066
|
|
|
971
1067
|
handleEvent(client, message) {
|
|
972
1068
|
if (!this.requireAuth(client)) return;
|
|
1069
|
+
if (client.role === "observer") {
|
|
1070
|
+
this.sendError(client.ws, "Observers are read-only", false, "EVENT_OBSERVER_READONLY");
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
973
1073
|
if (!client.subscriberId) {
|
|
974
1074
|
this.sendError(client.ws, "Unknown subscriber", false, "SUBSCRIBER_UNKNOWN");
|
|
975
1075
|
return;
|
|
@@ -1026,14 +1126,19 @@ class OnlineServer extends EventEmitter {
|
|
|
1026
1126
|
this.sendError(client.ws, "Room not found", false, "ROOM_NOT_FOUND");
|
|
1027
1127
|
return;
|
|
1028
1128
|
}
|
|
1029
|
-
|
|
1030
|
-
if (
|
|
1031
|
-
this.send(
|
|
1129
|
+
const broadcastRoom = (recipient) => {
|
|
1130
|
+
if (recipient !== client) {
|
|
1131
|
+
this.send(recipient.ws, payload);
|
|
1032
1132
|
if (payload.payload && payload.payload.kind === "wake") {
|
|
1033
|
-
this.send(
|
|
1133
|
+
this.send(recipient.ws, { type: "wake", from: client.subscriberId });
|
|
1034
1134
|
}
|
|
1035
1135
|
}
|
|
1036
|
-
}
|
|
1136
|
+
};
|
|
1137
|
+
room.members.forEach(broadcastRoom);
|
|
1138
|
+
if (room.observers) room.observers.forEach(broadcastRoom);
|
|
1139
|
+
if (kind === "message") {
|
|
1140
|
+
this.recordRoomMessage(payload.room, room, client, payload);
|
|
1141
|
+
}
|
|
1037
1142
|
return;
|
|
1038
1143
|
}
|
|
1039
1144
|
|
|
@@ -1050,20 +1155,20 @@ class OnlineServer extends EventEmitter {
|
|
|
1050
1155
|
this.sendError(client.ws, "Join channel first", false, "NOT_IN_CHANNEL");
|
|
1051
1156
|
return;
|
|
1052
1157
|
}
|
|
1053
|
-
const members = channel ? channel.members : null;
|
|
1054
|
-
if (!members || members.size === 0) return;
|
|
1055
1158
|
payload.channel = channelId;
|
|
1056
1159
|
if (kind === "message") {
|
|
1057
1160
|
this.recordChannelMessage(channelId, channel, client, payload);
|
|
1058
1161
|
}
|
|
1059
|
-
|
|
1060
|
-
if (
|
|
1061
|
-
this.send(
|
|
1162
|
+
const broadcastChannel = (recipient) => {
|
|
1163
|
+
if (recipient !== client) {
|
|
1164
|
+
this.send(recipient.ws, payload);
|
|
1062
1165
|
if (payload.payload && payload.payload.kind === "wake") {
|
|
1063
|
-
this.send(
|
|
1166
|
+
this.send(recipient.ws, { type: "wake", from: client.subscriberId });
|
|
1064
1167
|
}
|
|
1065
1168
|
}
|
|
1066
|
-
}
|
|
1169
|
+
};
|
|
1170
|
+
if (channel.members) channel.members.forEach(broadcastChannel);
|
|
1171
|
+
if (channel.observers) channel.observers.forEach(broadcastChannel);
|
|
1067
1172
|
return;
|
|
1068
1173
|
}
|
|
1069
1174
|
|
|
@@ -1113,7 +1218,12 @@ class OnlineServer extends EventEmitter {
|
|
|
1113
1218
|
return;
|
|
1114
1219
|
}
|
|
1115
1220
|
|
|
1116
|
-
|
|
1221
|
+
if (client.role === "observer") {
|
|
1222
|
+
if (!room.observers) room.observers = new Set();
|
|
1223
|
+
room.observers.add(client);
|
|
1224
|
+
} else {
|
|
1225
|
+
room.members.add(client);
|
|
1226
|
+
}
|
|
1117
1227
|
client.rooms.add(roomId);
|
|
1118
1228
|
this.send(client.ws, { type: "join_ack", ok: true, room: roomId });
|
|
1119
1229
|
}
|
|
@@ -1127,6 +1237,7 @@ class OnlineServer extends EventEmitter {
|
|
|
1127
1237
|
const room = this.rooms.get(roomId);
|
|
1128
1238
|
if (room) {
|
|
1129
1239
|
room.members.delete(client);
|
|
1240
|
+
if (room.observers) room.observers.delete(client);
|
|
1130
1241
|
}
|
|
1131
1242
|
client.rooms.delete(roomId);
|
|
1132
1243
|
this.send(client.ws, { type: "leave_ack", ok: true, room: roomId });
|
|
@@ -1144,6 +1255,7 @@ class OnlineServer extends EventEmitter {
|
|
|
1144
1255
|
const channelInfo = this.channels.get(channel);
|
|
1145
1256
|
if (channelInfo) {
|
|
1146
1257
|
channelInfo.members.delete(client);
|
|
1258
|
+
if (channelInfo.observers) channelInfo.observers.delete(client);
|
|
1147
1259
|
}
|
|
1148
1260
|
});
|
|
1149
1261
|
client.channels.clear();
|
|
@@ -1153,6 +1265,7 @@ class OnlineServer extends EventEmitter {
|
|
|
1153
1265
|
const room = this.rooms.get(roomId);
|
|
1154
1266
|
if (room) {
|
|
1155
1267
|
room.members.delete(client);
|
|
1268
|
+
if (room.observers) room.observers.delete(client);
|
|
1156
1269
|
}
|
|
1157
1270
|
});
|
|
1158
1271
|
client.rooms.clear();
|