mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 16:54:31 +00:00
feat(signal): add typing + read receipts
This commit is contained in:
@@ -22,6 +22,10 @@ export type SignalSendResult = {
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export type SignalRpcOpts = Pick<SignalSendOpts, "baseUrl" | "account" | "accountId" | "timeoutMs">;
|
||||
|
||||
export type SignalReceiptType = "read" | "viewed";
|
||||
|
||||
type SignalTarget =
|
||||
| { type: "recipient"; recipient: string }
|
||||
| { type: "group"; groupId: string }
|
||||
@@ -50,6 +54,59 @@ function parseTarget(raw: string): SignalTarget {
|
||||
return { type: "recipient", recipient: value };
|
||||
}
|
||||
|
||||
type SignalTargetParams = {
|
||||
recipient?: string[];
|
||||
groupId?: string;
|
||||
username?: string[];
|
||||
};
|
||||
|
||||
type SignalTargetAllowlist = {
|
||||
recipient?: boolean;
|
||||
group?: boolean;
|
||||
username?: boolean;
|
||||
};
|
||||
|
||||
function buildTargetParams(
|
||||
target: SignalTarget,
|
||||
allow: SignalTargetAllowlist,
|
||||
): SignalTargetParams | null {
|
||||
if (target.type === "recipient") {
|
||||
if (!allow.recipient) return null;
|
||||
return { recipient: [target.recipient] };
|
||||
}
|
||||
if (target.type === "group") {
|
||||
if (!allow.group) return null;
|
||||
return { groupId: target.groupId };
|
||||
}
|
||||
if (target.type === "username") {
|
||||
if (!allow.username) return null;
|
||||
return { username: [target.username] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveSignalRpcContext(
|
||||
opts: SignalRpcOpts,
|
||||
accountInfo?: ReturnType<typeof resolveSignalAccount>,
|
||||
) {
|
||||
const hasBaseUrl = Boolean(opts.baseUrl?.trim());
|
||||
const hasAccount = Boolean(opts.account?.trim());
|
||||
const resolvedAccount =
|
||||
accountInfo ||
|
||||
(!hasBaseUrl || !hasAccount
|
||||
? resolveSignalAccount({
|
||||
cfg: loadConfig(),
|
||||
accountId: opts.accountId,
|
||||
})
|
||||
: undefined);
|
||||
const baseUrl = opts.baseUrl?.trim() || resolvedAccount?.baseUrl;
|
||||
if (!baseUrl) {
|
||||
throw new Error("Signal base URL is required");
|
||||
}
|
||||
const account = opts.account?.trim() || resolvedAccount?.config.account?.trim();
|
||||
return { baseUrl, account };
|
||||
}
|
||||
|
||||
async function resolveAttachment(
|
||||
mediaUrl: string,
|
||||
maxBytes: number,
|
||||
@@ -74,8 +131,7 @@ export async function sendMessageSignal(
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const baseUrl = opts.baseUrl?.trim() || accountInfo.baseUrl;
|
||||
const account = opts.account?.trim() || accountInfo.config.account?.trim();
|
||||
const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo);
|
||||
const target = parseTarget(to);
|
||||
let message = text ?? "";
|
||||
let messageFromPlaceholder = false;
|
||||
@@ -129,13 +185,15 @@ export async function sendMessageSignal(
|
||||
params.attachments = attachments;
|
||||
}
|
||||
|
||||
if (target.type === "recipient") {
|
||||
params.recipient = [target.recipient];
|
||||
} else if (target.type === "group") {
|
||||
params.groupId = target.groupId;
|
||||
} else if (target.type === "username") {
|
||||
params.username = [target.username];
|
||||
const targetParams = buildTargetParams(target, {
|
||||
recipient: true,
|
||||
group: true,
|
||||
username: true,
|
||||
});
|
||||
if (!targetParams) {
|
||||
throw new Error("Signal recipient is required");
|
||||
}
|
||||
Object.assign(params, targetParams);
|
||||
|
||||
const result = await signalRpcRequest<{ timestamp?: number }>("send", params, {
|
||||
baseUrl,
|
||||
@@ -147,3 +205,47 @@ export async function sendMessageSignal(
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendTypingSignal(
|
||||
to: string,
|
||||
opts: SignalRpcOpts & { stop?: boolean } = {},
|
||||
): Promise<boolean> {
|
||||
const { baseUrl, account } = resolveSignalRpcContext(opts);
|
||||
const targetParams = buildTargetParams(parseTarget(to), {
|
||||
recipient: true,
|
||||
group: true,
|
||||
});
|
||||
if (!targetParams) return false;
|
||||
const params: Record<string, unknown> = { ...targetParams };
|
||||
if (account) params.account = account;
|
||||
if (opts.stop) params.stop = true;
|
||||
await signalRpcRequest("sendTyping", params, {
|
||||
baseUrl,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function sendReadReceiptSignal(
|
||||
to: string,
|
||||
targetTimestamp: number,
|
||||
opts: SignalRpcOpts & { type?: SignalReceiptType } = {},
|
||||
): Promise<boolean> {
|
||||
if (!Number.isFinite(targetTimestamp) || targetTimestamp <= 0) return false;
|
||||
const { baseUrl, account } = resolveSignalRpcContext(opts);
|
||||
const targetParams = buildTargetParams(parseTarget(to), {
|
||||
recipient: true,
|
||||
});
|
||||
if (!targetParams) return false;
|
||||
const params: Record<string, unknown> = {
|
||||
...targetParams,
|
||||
targetTimestamp,
|
||||
type: opts.type ?? "read",
|
||||
};
|
||||
if (account) params.account = account;
|
||||
await signalRpcRequest("sendReceipt", params, {
|
||||
baseUrl,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user