mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Signal: normalize mention placeholders
This commit is contained in:
committed by
Vignesh
parent
051c574047
commit
cfec19df53
@@ -53,6 +53,14 @@ type GroupEventOpts = {
|
||||
message?: string;
|
||||
attachments?: unknown[];
|
||||
quoteText?: string;
|
||||
mentions?:
|
||||
| Array<{
|
||||
uuid?: string;
|
||||
number?: string;
|
||||
start?: number;
|
||||
length?: number;
|
||||
}>
|
||||
| null;
|
||||
};
|
||||
|
||||
function makeGroupEvent(opts: GroupEventOpts) {
|
||||
@@ -67,6 +75,7 @@ function makeGroupEvent(opts: GroupEventOpts) {
|
||||
message: opts.message ?? "",
|
||||
attachments: opts.attachments ?? [],
|
||||
quote: opts.quoteText ? { text: opts.quoteText } : undefined,
|
||||
mentions: opts.mentions ?? undefined,
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
},
|
||||
@@ -203,4 +212,63 @@ describe("signal mention gating", () => {
|
||||
await handler(makeGroupEvent({ message: "/help" }));
|
||||
expect(capturedCtx).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hydrates mention placeholders before trimming so offsets stay aligned", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: false } } } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const placeholder = "\uFFFC";
|
||||
const message = `\n${placeholder} hi ${placeholder}`;
|
||||
const firstStart = message.indexOf(placeholder);
|
||||
const secondStart = message.indexOf(placeholder, firstStart + 1);
|
||||
|
||||
await handler(
|
||||
makeGroupEvent({
|
||||
message,
|
||||
mentions: [
|
||||
{ uuid: "123e4567", start: firstStart, length: placeholder.length },
|
||||
{ number: "+15550002222", start: secondStart, length: placeholder.length },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(capturedCtx).toBeTruthy();
|
||||
const body = String(capturedCtx?.Body ?? "");
|
||||
expect(body).toContain("@123e4567 hi @+15550002222");
|
||||
expect(body).not.toContain(placeholder);
|
||||
});
|
||||
|
||||
it("counts mention metadata replacements toward requireMention gating", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@123e4567"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const placeholder = "\uFFFC";
|
||||
const message = ` ${placeholder} ping`;
|
||||
const start = message.indexOf(placeholder);
|
||||
|
||||
await handler(
|
||||
makeGroupEvent({
|
||||
message,
|
||||
mentions: [{ uuid: "123e4567", start, length: placeholder.length }],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(capturedCtx).toBeTruthy();
|
||||
expect(String(capturedCtx?.Body ?? "")).toContain("@123e4567");
|
||||
expect(capturedCtx?.WasMentioned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
resolveSignalSender,
|
||||
} from "../identity.js";
|
||||
import { sendMessageSignal, sendReadReceiptSignal, sendTypingSignal } from "../send.js";
|
||||
|
||||
import { renderSignalMentions } from "./mentions.js";
|
||||
export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
const inboundDebounceMs = resolveInboundDebounceMs({ cfg: deps.cfg, channel: "signal" });
|
||||
|
||||
@@ -354,20 +354,10 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
: null;
|
||||
|
||||
// Replace  (object replacement character) with @uuid or @phone from mentions
|
||||
let messageText = (dataMessage?.message ?? "").trim();
|
||||
if (messageText && dataMessage?.mentions?.length) {
|
||||
const mentions = dataMessage.mentions
|
||||
.filter((m) => (m.uuid || m.number) && m.start != null && m.length != null)
|
||||
.sort((a, b) => (b.start ?? 0) - (a.start ?? 0)); // Reverse order to avoid index shifting
|
||||
|
||||
for (const mention of mentions) {
|
||||
const start = mention.start!;
|
||||
const length = mention.length!;
|
||||
const identifier = mention.uuid || mention.number || "";
|
||||
const replacement = `@${identifier}`;
|
||||
messageText = messageText.slice(0, start) + replacement + messageText.slice(start + length);
|
||||
}
|
||||
}
|
||||
// Signal encodes mentions as the object replacement character; hydrate them from metadata first.
|
||||
const rawMessage = dataMessage?.message ?? "";
|
||||
const normalizedMessage = renderSignalMentions(rawMessage, dataMessage?.mentions);
|
||||
const messageText = normalizedMessage.trim();
|
||||
|
||||
const quoteText = dataMessage?.quote?.text?.trim() ?? "";
|
||||
const hasBodyContent =
|
||||
|
||||
34
src/signal/monitor/mentions.test.ts
Normal file
34
src/signal/monitor/mentions.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { renderSignalMentions } from "./mentions.js";
|
||||
|
||||
const PLACEHOLDER = "\uFFFC";
|
||||
|
||||
describe("renderSignalMentions", () => {
|
||||
it("returns the original message when no mentions are provided", () => {
|
||||
const message = `${PLACEHOLDER} ping`;
|
||||
expect(renderSignalMentions(message, null)).toBe(message);
|
||||
expect(renderSignalMentions(message, [])).toBe(message);
|
||||
});
|
||||
|
||||
it("replaces placeholder code points using mention metadata", () => {
|
||||
const message = `${PLACEHOLDER} hi ${PLACEHOLDER}!`;
|
||||
const normalized = renderSignalMentions(message, [
|
||||
{ uuid: "abc-123", start: 0, length: 1 },
|
||||
{ number: "+15550005555", start: message.lastIndexOf(PLACEHOLDER), length: 1 },
|
||||
]);
|
||||
|
||||
expect(normalized).toBe("@abc-123 hi @+15550005555!");
|
||||
});
|
||||
|
||||
it("skips mentions that lack identifiers or out-of-bounds spans", () => {
|
||||
const message = `${PLACEHOLDER} hi`;
|
||||
const normalized = renderSignalMentions(message, [
|
||||
{ name: "ignored" },
|
||||
{ uuid: "valid", start: 0, length: 1 },
|
||||
{ number: "+1555", start: 999, length: 1 },
|
||||
]);
|
||||
|
||||
expect(normalized).toBe("@valid hi");
|
||||
});
|
||||
});
|
||||
42
src/signal/monitor/mentions.ts
Normal file
42
src/signal/monitor/mentions.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { SignalMention } from "./event-handler.types.js";
|
||||
|
||||
const OBJECT_REPLACEMENT = "\uFFFC";
|
||||
|
||||
function isValidMention(mention: SignalMention | null | undefined): mention is SignalMention {
|
||||
if (!mention) return false;
|
||||
if (!(mention.uuid || mention.number)) return false;
|
||||
if (typeof mention.start !== "number" || Number.isNaN(mention.start)) return false;
|
||||
if (typeof mention.length !== "number" || Number.isNaN(mention.length)) return false;
|
||||
return mention.length > 0;
|
||||
}
|
||||
|
||||
function clampBounds(start: number, length: number, textLength: number) {
|
||||
const safeStart = Math.max(0, Math.trunc(start));
|
||||
const safeLength = Math.max(0, Math.trunc(length));
|
||||
const safeEnd = Math.min(textLength, safeStart + safeLength);
|
||||
return { start: safeStart, end: safeEnd };
|
||||
}
|
||||
|
||||
export function renderSignalMentions(message: string, mentions?: SignalMention[] | null) {
|
||||
if (!message || !mentions?.length) {
|
||||
return message;
|
||||
}
|
||||
|
||||
let normalized = message;
|
||||
const candidates = mentions.filter(isValidMention).sort((a, b) => b.start! - a.start!);
|
||||
|
||||
for (const mention of candidates) {
|
||||
const identifier = mention.uuid ?? mention.number;
|
||||
if (!identifier) continue;
|
||||
|
||||
const { start, end } = clampBounds(mention.start!, mention.length!, normalized.length);
|
||||
if (start >= end) continue;
|
||||
const slice = normalized.slice(start, end);
|
||||
|
||||
if (!slice.includes(OBJECT_REPLACEMENT)) continue;
|
||||
|
||||
normalized = normalized.slice(0, start) + `@${identifier}` + normalized.slice(end);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
Reference in New Issue
Block a user