feat(signal): add typing + read receipts
This commit is contained in:
@@ -34,6 +34,8 @@ describe("signal createSignalEventHandler inbound contract", () => {
|
||||
reactionAllowlist: [],
|
||||
mediaMaxBytes: 1024,
|
||||
ignoreAttachments: true,
|
||||
sendReadReceipts: false,
|
||||
readReceiptsViaDaemon: false,
|
||||
fetchAttachment: async () => null,
|
||||
deliverReplies: async () => {},
|
||||
resolveSignalReactionTargets: () => [],
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
clearHistoryEntries,
|
||||
} from "../../auto-reply/reply/history.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import {
|
||||
readSessionUpdatedAt,
|
||||
recordSessionMetaFromInbound,
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
resolveSignalRecipient,
|
||||
resolveSignalSender,
|
||||
} from "../identity.js";
|
||||
import { sendMessageSignal } from "../send.js";
|
||||
import { sendMessageSignal, sendReadReceiptSignal, sendTypingSignal } from "../send.js";
|
||||
|
||||
import type { SignalEventHandlerDeps, SignalReceivePayload } from "./event-handler.types.js";
|
||||
|
||||
@@ -190,7 +190,20 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
identityName: resolveIdentityName(deps.cfg, route.agentId),
|
||||
};
|
||||
|
||||
const dispatcher = createReplyDispatcher({
|
||||
const onReplyStart = async () => {
|
||||
try {
|
||||
if (!ctxPayload.To) return;
|
||||
await sendTypingSignal(ctxPayload.To, {
|
||||
baseUrl: deps.baseUrl,
|
||||
account: deps.account,
|
||||
accountId: deps.accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`signal typing cue failed for ${ctxPayload.To}: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
|
||||
responsePrefix: resolveEffectiveMessagesConfig(deps.cfg, route.agentId).responsePrefix,
|
||||
responsePrefixContextProvider: () => prefixContext,
|
||||
humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId),
|
||||
@@ -209,6 +222,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
onError: (err, info) => {
|
||||
deps.runtime.error?.(danger(`signal ${info.kind} reply failed: ${String(err)}`));
|
||||
},
|
||||
onReplyStart,
|
||||
});
|
||||
|
||||
const { queuedFinal } = await dispatchReplyFromConfig({
|
||||
@@ -216,6 +230,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
cfg: deps.cfg,
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
...replyOptions,
|
||||
disableBlockStreaming:
|
||||
typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined,
|
||||
onModelSelected: (ctx) => {
|
||||
@@ -227,6 +242,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
},
|
||||
},
|
||||
});
|
||||
markDispatchIdle();
|
||||
if (!queuedFinal) {
|
||||
if (entry.isGroup && historyKey && deps.historyLimit > 0) {
|
||||
clearHistoryEntries({ historyMap: deps.groupHistories, historyKey });
|
||||
@@ -479,6 +495,31 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
const bodyText = messageText || placeholder || dataMessage.quote?.text?.trim() || "";
|
||||
if (!bodyText) return;
|
||||
|
||||
const receiptTimestamp =
|
||||
typeof envelope.timestamp === "number"
|
||||
? envelope.timestamp
|
||||
: typeof dataMessage.timestamp === "number"
|
||||
? dataMessage.timestamp
|
||||
: undefined;
|
||||
if (deps.sendReadReceipts && !deps.readReceiptsViaDaemon && !isGroup && receiptTimestamp) {
|
||||
try {
|
||||
await sendReadReceiptSignal(`signal:${senderRecipient}`, receiptTimestamp, {
|
||||
baseUrl: deps.baseUrl,
|
||||
account: deps.account,
|
||||
accountId: deps.accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`signal read receipt failed for ${senderDisplay}: ${String(err)}`);
|
||||
}
|
||||
} else if (
|
||||
deps.sendReadReceipts &&
|
||||
!deps.readReceiptsViaDaemon &&
|
||||
!isGroup &&
|
||||
!receiptTimestamp
|
||||
) {
|
||||
logVerbose(`signal read receipt skipped (missing timestamp) for ${senderDisplay}`);
|
||||
}
|
||||
|
||||
const senderName = envelope.sourceName ?? senderDisplay;
|
||||
const messageId =
|
||||
typeof envelope.timestamp === "number" ? String(envelope.timestamp) : undefined;
|
||||
|
||||
@@ -76,6 +76,8 @@ export type SignalEventHandlerDeps = {
|
||||
reactionAllowlist: string[];
|
||||
mediaMaxBytes: number;
|
||||
ignoreAttachments: boolean;
|
||||
sendReadReceipts: boolean;
|
||||
readReceiptsViaDaemon: boolean;
|
||||
fetchAttachment: (params: {
|
||||
baseUrl: string;
|
||||
account?: string;
|
||||
|
||||
Reference in New Issue
Block a user