@tinyclaw/plugin-channel-friends 1.1.0-staging.df9eaf8 → 2.0.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/dist/index.js +24 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +68 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ const friendsPlugin = {
|
|
|
42
42
|
description: 'Invite-based web chat for friends — powered by FRIENDS.md',
|
|
43
43
|
type: 'channel',
|
|
44
44
|
version: '0.1.0',
|
|
45
|
+
channelPrefix: 'friend',
|
|
45
46
|
getPairingTools(_secrets, configManager) {
|
|
46
47
|
// Initialize the store early so pairing tools work before start()
|
|
47
48
|
if (!store) {
|
|
@@ -87,6 +88,29 @@ const friendsPlugin = {
|
|
|
87
88
|
await friendsServer.start();
|
|
88
89
|
logger.info(`Friends chat available at http://${host}:${port}/chat`);
|
|
89
90
|
},
|
|
91
|
+
async sendToUser(userId, message) {
|
|
92
|
+
if (!friendsServer) {
|
|
93
|
+
throw new Error('Friends server is not running');
|
|
94
|
+
}
|
|
95
|
+
// Parse username from the prefixed format "friend:<username>"
|
|
96
|
+
const username = userId.replace(/^friend:/, '');
|
|
97
|
+
if (!username) {
|
|
98
|
+
throw new Error(`Invalid Friends userId: ${userId}`);
|
|
99
|
+
}
|
|
100
|
+
const delivered = friendsServer.pushToUser(username, {
|
|
101
|
+
type: 'nudge',
|
|
102
|
+
content: message.content,
|
|
103
|
+
source: message.source,
|
|
104
|
+
priority: message.priority,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
});
|
|
107
|
+
if (!delivered) {
|
|
108
|
+
logger.warn(`Friends: user "${username}" has no active push connections`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
logger.info(`Friends: sent outbound message to ${userId}`);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
90
114
|
async stop() {
|
|
91
115
|
if (friendsServer) {
|
|
92
116
|
friendsServer.stop();
|
package/dist/server.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ interface FriendsServerConfig {
|
|
|
24
24
|
export declare function createFriendsServer(config: FriendsServerConfig): {
|
|
25
25
|
start(): Promise<void>;
|
|
26
26
|
stop(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Push an outbound message to all connected SSE clients for a username.
|
|
29
|
+
* Returns true if at least one client received the message.
|
|
30
|
+
*/
|
|
31
|
+
pushToUser(username: string, data: Record<string, unknown>): boolean;
|
|
27
32
|
getPort(): number;
|
|
28
33
|
};
|
|
29
34
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -68,6 +68,7 @@ export function createFriendsServer(config) {
|
|
|
68
68
|
const { port, host, store, chatHtml, onMessage, onMessageStream } = config;
|
|
69
69
|
const secure = config.secure ?? (process.env.NODE_ENV === 'production');
|
|
70
70
|
const textEncoder = new TextEncoder();
|
|
71
|
+
const pushClients = new Map();
|
|
71
72
|
let server = null;
|
|
72
73
|
return {
|
|
73
74
|
async start() {
|
|
@@ -168,6 +169,48 @@ export function createFriendsServer(config) {
|
|
|
168
169
|
store.updateNickname(friend.username, newNickname);
|
|
169
170
|
return jsonResponse({ ok: true, nickname: newNickname });
|
|
170
171
|
}
|
|
172
|
+
// ─── Push SSE stream (outbound notifications) ──────────
|
|
173
|
+
if (pathname === '/api/push' && request.method === 'GET') {
|
|
174
|
+
const friend = authenticateFriend(request, store);
|
|
175
|
+
if (!friend) {
|
|
176
|
+
return jsonResponse({ error: 'Unauthorized' }, 401);
|
|
177
|
+
}
|
|
178
|
+
const username = friend.username;
|
|
179
|
+
const stream = new ReadableStream({
|
|
180
|
+
start(controller) {
|
|
181
|
+
const pushClient = { controller, encoder: textEncoder };
|
|
182
|
+
if (!pushClients.has(username)) {
|
|
183
|
+
pushClients.set(username, new Set());
|
|
184
|
+
}
|
|
185
|
+
pushClients.get(username).add(pushClient);
|
|
186
|
+
// Heartbeat to keep connection alive
|
|
187
|
+
const heartbeat = setInterval(() => {
|
|
188
|
+
try {
|
|
189
|
+
controller.enqueue(textEncoder.encode(': heartbeat\n\n'));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
clearInterval(heartbeat);
|
|
193
|
+
pushClients.get(username)?.delete(pushClient);
|
|
194
|
+
}
|
|
195
|
+
}, 15_000);
|
|
196
|
+
// Clean up on abort
|
|
197
|
+
request.signal.addEventListener('abort', () => {
|
|
198
|
+
clearInterval(heartbeat);
|
|
199
|
+
pushClients.get(username)?.delete(pushClient);
|
|
200
|
+
if (pushClients.get(username)?.size === 0) {
|
|
201
|
+
pushClients.delete(username);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return new Response(stream, {
|
|
207
|
+
headers: {
|
|
208
|
+
'Content-Type': 'text/event-stream',
|
|
209
|
+
'Cache-Control': 'no-cache',
|
|
210
|
+
Connection: 'keep-alive',
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
171
214
|
// ─── Chat API (message) ─────────────────────────────────
|
|
172
215
|
if (pathname === '/api/chat' && request.method === 'POST') {
|
|
173
216
|
const friend = authenticateFriend(request, store);
|
|
@@ -283,9 +326,34 @@ export function createFriendsServer(config) {
|
|
|
283
326
|
if (server) {
|
|
284
327
|
server.stop();
|
|
285
328
|
server = null;
|
|
329
|
+
// Clean up all push clients
|
|
330
|
+
pushClients.clear();
|
|
286
331
|
logger.info('Friends chat server stopped');
|
|
287
332
|
}
|
|
288
333
|
},
|
|
334
|
+
/**
|
|
335
|
+
* Push an outbound message to all connected SSE clients for a username.
|
|
336
|
+
* Returns true if at least one client received the message.
|
|
337
|
+
*/
|
|
338
|
+
pushToUser(username, data) {
|
|
339
|
+
const clients = pushClients.get(username);
|
|
340
|
+
if (!clients || clients.size === 0)
|
|
341
|
+
return false;
|
|
342
|
+
const payload = `data: ${JSON.stringify(data)}\n\n`;
|
|
343
|
+
const dead = [];
|
|
344
|
+
for (const pc of clients) {
|
|
345
|
+
try {
|
|
346
|
+
pc.controller.enqueue(pc.encoder.encode(payload));
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
dead.push(pc);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const dc of dead) {
|
|
353
|
+
clients.delete(dc);
|
|
354
|
+
}
|
|
355
|
+
return clients.size > 0 || dead.length < (clients.size + dead.length);
|
|
356
|
+
},
|
|
289
357
|
getPort() {
|
|
290
358
|
return server?.port || port;
|
|
291
359
|
},
|