u-foo 1.4.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 +27 -0
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
|
@@ -185,6 +185,14 @@ class OnlineServer extends EventEmitter {
|
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
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
|
+
|
|
188
196
|
if (pathname.startsWith("/ufoo/online/rooms")) {
|
|
189
197
|
// Step 4: HTTP auth
|
|
190
198
|
if (!this.authenticateHttp(req, res)) return;
|
|
@@ -617,6 +625,25 @@ class OnlineServer extends EventEmitter {
|
|
|
617
625
|
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
618
626
|
}
|
|
619
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
|
+
|
|
620
647
|
handleChannelsRequest(req, res) {
|
|
621
648
|
if (req.method === "GET") {
|
|
622
649
|
this.sendJson(res, 200, { ok: true, channels: this.listChannels() });
|