@unicitylabs/openclaw-unicity 0.3.0 → 0.3.2
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/channel.ts +91 -15
- package/src/tools/create-private-group.ts +4 -2
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -10,6 +10,25 @@ import type { UnicityConfig } from "./config.js";
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
12
12
|
|
|
13
|
+
/** How long (ms) to wait after the last group message before declaring backfill complete. */
|
|
14
|
+
export const GROUP_BACKFILL_DEBOUNCE_MS = 3_000;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
interface GroupBackfillState {
|
|
18
|
+
phase: "buffering" | "live";
|
|
19
|
+
latestMsg: {
|
|
20
|
+
id?: string;
|
|
21
|
+
groupId: string;
|
|
22
|
+
senderPubkey: string;
|
|
23
|
+
senderNametag?: string;
|
|
24
|
+
content: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
replyToId?: string;
|
|
27
|
+
} | null;
|
|
28
|
+
bufferedCount: number;
|
|
29
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
// ---------------------------------------------------------------------------
|
|
14
33
|
// Account config shape (read from openclaw config under channels.unicity)
|
|
15
34
|
// ---------------------------------------------------------------------------
|
|
@@ -422,21 +441,22 @@ export const unicityChannelPlugin = {
|
|
|
422
441
|
});
|
|
423
442
|
});
|
|
424
443
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
444
|
+
// -- Group message dispatch helper & backfill debounce --------------------
|
|
445
|
+
|
|
446
|
+
type GroupMsg = {
|
|
447
|
+
id?: string;
|
|
428
448
|
groupId: string;
|
|
429
|
-
groupName?: string;
|
|
430
449
|
senderPubkey: string;
|
|
431
450
|
senderNametag?: string;
|
|
432
451
|
content: string;
|
|
433
|
-
timestamp
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (msg.senderPubkey === sphere.identity?.chainPubkey) return;
|
|
452
|
+
timestamp: number;
|
|
453
|
+
replyToId?: string;
|
|
454
|
+
};
|
|
437
455
|
|
|
456
|
+
function dispatchGroupMessage(msg: GroupMsg): void {
|
|
438
457
|
const senderName = msg.senderNametag ?? msg.senderPubkey.slice(0, 12);
|
|
439
|
-
const
|
|
458
|
+
const groupData = sphere.groupChat?.getGroup?.(msg.groupId);
|
|
459
|
+
const groupName = groupData?.name ?? msg.groupId;
|
|
440
460
|
const isOwner = isSenderOwner(msg.senderPubkey, msg.senderNametag);
|
|
441
461
|
const metadataHeader = `[SenderName: ${senderName} | SenderId: ${msg.senderPubkey} | GroupId: ${msg.groupId} | GroupName: ${groupName} | IsOwner: ${isOwner} | CommandAuthorized: ${isOwner}]`;
|
|
442
462
|
const sanitizedContent = msg.content.replace(/\[(?:SenderName|SenderId|IsOwner|CommandAuthorized|GroupId|GroupName)\s*:/gi, "[BLOCKED:");
|
|
@@ -487,40 +507,95 @@ export const unicityChannelPlugin = {
|
|
|
487
507
|
.catch((err: unknown) => {
|
|
488
508
|
ctx.log?.error(`[${ctx.account.accountId}] Group message dispatch error: ${err}`);
|
|
489
509
|
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Nostr pubkey for self-message detection. Group messages use the 32-byte
|
|
513
|
+
// x-only Nostr pubkey (event.pubkey), NOT the 33-byte compressed chainPubkey.
|
|
514
|
+
// sphere.groupChat.getMyPublicKey() returns the correct Nostr-format key.
|
|
515
|
+
const myNostrPubkey = sphere.groupChat?.getMyPublicKey?.() ?? null;
|
|
516
|
+
|
|
517
|
+
// Per-group backfill state: buffer messages during the initial burst, then
|
|
518
|
+
// switch to live dispatch once the burst settles.
|
|
519
|
+
const groupBackfillStates = new Map<string, GroupBackfillState>();
|
|
520
|
+
|
|
521
|
+
// Subscribe to incoming group messages
|
|
522
|
+
const unsubGroupMessage = sphere.groupChat?.onMessage?.((msg: GroupMsg) => {
|
|
523
|
+
// Skip messages from self (echoed back by the relay).
|
|
524
|
+
// Compare against the Nostr x-only pubkey, not chainPubkey.
|
|
525
|
+
if (myNostrPubkey && msg.senderPubkey === myNostrPubkey) return;
|
|
526
|
+
|
|
527
|
+
// Lookup or create per-group backfill state
|
|
528
|
+
let state = groupBackfillStates.get(msg.groupId);
|
|
529
|
+
if (!state) {
|
|
530
|
+
state = { phase: "buffering", latestMsg: null, bufferedCount: 0, timer: null };
|
|
531
|
+
groupBackfillStates.set(msg.groupId, state);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Already past backfill — dispatch immediately
|
|
535
|
+
if (state.phase === "live") {
|
|
536
|
+
dispatchGroupMessage(msg);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// BUFFERING: keep only the latest message, reset the debounce timer
|
|
541
|
+
state.latestMsg = msg;
|
|
542
|
+
state.bufferedCount++;
|
|
543
|
+
if (state.timer) clearTimeout(state.timer);
|
|
544
|
+
state.timer = setTimeout(() => {
|
|
545
|
+
state!.phase = "live";
|
|
546
|
+
state!.timer = null;
|
|
547
|
+
ctx.log?.info(
|
|
548
|
+
`[${ctx.account.accountId}] Group backfill settled for ${msg.groupId}, ${state!.bufferedCount} message(s) buffered`,
|
|
549
|
+
);
|
|
550
|
+
// Dispatch the most recent buffered message so the agent has context
|
|
551
|
+
if (state!.latestMsg) {
|
|
552
|
+
dispatchGroupMessage(state!.latestMsg);
|
|
553
|
+
state!.latestMsg = null;
|
|
554
|
+
}
|
|
555
|
+
}, GROUP_BACKFILL_DEBOUNCE_MS);
|
|
490
556
|
}) ?? (() => {});
|
|
491
557
|
|
|
492
558
|
// Subscribe to group lifecycle events and notify owner
|
|
493
|
-
const unsubGroupJoined = sphere.on?.("groupchat:joined", (event: {
|
|
559
|
+
const unsubGroupJoined = sphere.on?.("groupchat:joined", (event: { groupId: string; groupName: string }) => {
|
|
494
560
|
const owner = getOwnerIdentity();
|
|
495
561
|
if (owner) {
|
|
496
|
-
const label = event.
|
|
562
|
+
const label = event.groupName ? `${event.groupName} (${event.groupId})` : event.groupId;
|
|
497
563
|
sphere.communications.sendDM(`@${owner}`, `I joined group ${label}`).catch((err) => {
|
|
498
564
|
ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group join: ${err}`);
|
|
499
565
|
});
|
|
500
566
|
}
|
|
501
567
|
}) ?? (() => {});
|
|
502
568
|
|
|
503
|
-
const unsubGroupLeft = sphere.on?.("groupchat:left", (event: {
|
|
569
|
+
const unsubGroupLeft = sphere.on?.("groupchat:left", (event: { groupId: string }) => {
|
|
504
570
|
const owner = getOwnerIdentity();
|
|
505
571
|
if (owner) {
|
|
506
|
-
const
|
|
572
|
+
const groupData = sphere.groupChat?.getGroup?.(event.groupId);
|
|
573
|
+
const label = groupData?.name ?? event.groupId;
|
|
507
574
|
sphere.communications.sendDM(`@${owner}`, `I left group ${label}`).catch((err) => {
|
|
508
575
|
ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group leave: ${err}`);
|
|
509
576
|
});
|
|
510
577
|
}
|
|
511
578
|
}) ?? (() => {});
|
|
512
579
|
|
|
513
|
-
const unsubGroupKicked = sphere.on?.("groupchat:kicked", (event: {
|
|
580
|
+
const unsubGroupKicked = sphere.on?.("groupchat:kicked", (event: { groupId: string; groupName: string }) => {
|
|
514
581
|
const owner = getOwnerIdentity();
|
|
515
582
|
if (owner) {
|
|
516
|
-
const label = event.
|
|
583
|
+
const label = event.groupName ? `${event.groupName} (${event.groupId})` : event.groupId;
|
|
517
584
|
sphere.communications.sendDM(`@${owner}`, `I was kicked from group ${label}`).catch((err) => {
|
|
518
585
|
ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group kick: ${err}`);
|
|
519
586
|
});
|
|
520
587
|
}
|
|
521
588
|
}) ?? (() => {});
|
|
522
589
|
|
|
590
|
+
function clearBackfillTimers(): void {
|
|
591
|
+
for (const state of groupBackfillStates.values()) {
|
|
592
|
+
if (state.timer) clearTimeout(state.timer);
|
|
593
|
+
}
|
|
594
|
+
groupBackfillStates.clear();
|
|
595
|
+
}
|
|
596
|
+
|
|
523
597
|
ctx.abortSignal.addEventListener("abort", () => {
|
|
598
|
+
clearBackfillTimers();
|
|
524
599
|
unsub();
|
|
525
600
|
unsubTransfer();
|
|
526
601
|
unsubPaymentRequest();
|
|
@@ -532,6 +607,7 @@ export const unicityChannelPlugin = {
|
|
|
532
607
|
|
|
533
608
|
return {
|
|
534
609
|
stop: () => {
|
|
610
|
+
clearBackfillTimers();
|
|
535
611
|
unsub();
|
|
536
612
|
unsubTransfer();
|
|
537
613
|
unsubPaymentRequest();
|
|
@@ -28,8 +28,10 @@ export const createPrivateGroupTool = {
|
|
|
28
28
|
visibility: "private",
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
const joinCode = await sphere.groupChat.createInvite(group.id);
|
|
32
|
+
if (!joinCode) {
|
|
33
|
+
throw new Error("Failed to generate invite code for the private group");
|
|
34
|
+
}
|
|
33
35
|
|
|
34
36
|
const lines = [
|
|
35
37
|
`Private group created: ${group.name} (${group.id})`,
|