fix(signal): stabilize daemon + add signal delivery
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import { chunkText } from "../auto-reply/chunk.js";
|
||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||
import { chunkText } from "../auto-reply/chunk.js";
|
||||
import { danger, isVerbose, logVerbose } from "../globals.js";
|
||||
import { mediaKindFromMime } from "../media/constants.js";
|
||||
import { saveMediaBuffer } from "../media/store.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { normalizeE164 } from "../utils.js";
|
||||
import { signalRpcRequest, streamSignalEvents } from "./client.js";
|
||||
import { signalCheck, signalRpcRequest, streamSignalEvents } from "./client.js";
|
||||
import { spawnSignalDaemon } from "./daemon.js";
|
||||
import { sendMessageSignal } from "./send.js";
|
||||
|
||||
@@ -93,10 +93,7 @@ function resolveAccount(opts: MonitorSignalOpts): string | undefined {
|
||||
function resolveAllowFrom(opts: MonitorSignalOpts): string[] {
|
||||
const cfg = loadConfig();
|
||||
const raw =
|
||||
opts.allowFrom ??
|
||||
cfg.signal?.allowFrom ??
|
||||
cfg.routing?.allowFrom ??
|
||||
[];
|
||||
opts.allowFrom ?? cfg.signal?.allowFrom ?? cfg.routing?.allowFrom ?? [];
|
||||
return raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -110,6 +107,32 @@ function isAllowedSender(sender: string, allowFrom: string[]): boolean {
|
||||
return normalizedAllow.includes(normalizedSender);
|
||||
}
|
||||
|
||||
async function waitForSignalDaemonReady(params: {
|
||||
baseUrl: string;
|
||||
abortSignal?: AbortSignal;
|
||||
timeoutMs: number;
|
||||
runtime: RuntimeEnv;
|
||||
}): Promise<void> {
|
||||
const started = Date.now();
|
||||
let lastError: string | null = null;
|
||||
|
||||
while (Date.now() - started < params.timeoutMs) {
|
||||
if (params.abortSignal?.aborted) return;
|
||||
const res = await signalCheck(params.baseUrl, 1000);
|
||||
if (res.ok) return;
|
||||
lastError =
|
||||
res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable");
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
|
||||
params.runtime.error?.(
|
||||
danger(
|
||||
`signal: daemon not ready after ${params.timeoutMs}ms (${lastError ?? "unknown error"})`,
|
||||
),
|
||||
);
|
||||
throw new Error(`signal daemon not ready (${lastError ?? "unknown error"})`);
|
||||
}
|
||||
|
||||
async function fetchAttachment(params: {
|
||||
baseUrl: string;
|
||||
account?: string;
|
||||
@@ -202,9 +225,7 @@ export async function monitorSignalProvider(
|
||||
opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments ?? false;
|
||||
|
||||
const autoStart =
|
||||
opts.autoStart ??
|
||||
cfg.signal?.autoStart ??
|
||||
(cfg.signal?.httpUrl ? false : true);
|
||||
opts.autoStart ?? cfg.signal?.autoStart ?? !cfg.signal?.httpUrl;
|
||||
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
||||
|
||||
if (autoStart) {
|
||||
@@ -220,8 +241,7 @@ export async function monitorSignalProvider(
|
||||
ignoreAttachments:
|
||||
opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments,
|
||||
ignoreStories: opts.ignoreStories ?? cfg.signal?.ignoreStories,
|
||||
sendReadReceipts:
|
||||
opts.sendReadReceipts ?? cfg.signal?.sendReadReceipts,
|
||||
sendReadReceipts: opts.sendReadReceipts ?? cfg.signal?.sendReadReceipts,
|
||||
runtime,
|
||||
});
|
||||
}
|
||||
@@ -232,6 +252,15 @@ export async function monitorSignalProvider(
|
||||
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
||||
|
||||
try {
|
||||
if (daemonHandle) {
|
||||
await waitForSignalDaemonReady({
|
||||
baseUrl,
|
||||
abortSignal: opts.abortSignal,
|
||||
timeoutMs: 10_000,
|
||||
runtime,
|
||||
});
|
||||
}
|
||||
|
||||
const handleEvent = async (event: { event?: string; data?: string }) => {
|
||||
if (event.event !== "receive" || !event.data) return;
|
||||
let payload: SignalReceivePayload | null = null;
|
||||
@@ -242,7 +271,9 @@ export async function monitorSignalProvider(
|
||||
return;
|
||||
}
|
||||
if (payload?.exception?.message) {
|
||||
runtime.error?.(`signal: receive exception: ${payload.exception.message}`);
|
||||
runtime.error?.(
|
||||
`signal: receive exception: ${payload.exception.message}`,
|
||||
);
|
||||
}
|
||||
const envelope = payload?.envelope;
|
||||
if (!envelope) return;
|
||||
@@ -282,7 +313,8 @@ export async function monitorSignalProvider(
|
||||
});
|
||||
if (fetched) {
|
||||
mediaPath = fetched.path;
|
||||
mediaType = fetched.contentType ?? firstAttachment.contentType ?? undefined;
|
||||
mediaType =
|
||||
fetched.contentType ?? firstAttachment.contentType ?? undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
runtime.error?.(
|
||||
@@ -317,7 +349,7 @@ export async function monitorSignalProvider(
|
||||
From: isGroup ? `group:${groupId}` : `signal:${sender}`,
|
||||
To: isGroup ? `group:${groupId}` : `signal:${sender}`,
|
||||
ChatType: isGroup ? "group" : "direct",
|
||||
GroupSubject: isGroup ? groupName ?? undefined : undefined,
|
||||
GroupSubject: isGroup ? (groupName ?? undefined) : undefined,
|
||||
SenderName: envelope.sourceName ?? sender,
|
||||
Surface: "signal" as const,
|
||||
MessageSid: envelope.timestamp ? String(envelope.timestamp) : undefined,
|
||||
|
||||
46
src/signal/probe.test.ts
Normal file
46
src/signal/probe.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { probeSignal } from "./probe.js";
|
||||
|
||||
const signalCheckMock = vi.fn();
|
||||
const signalRpcRequestMock = vi.fn();
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
signalCheck: (...args: unknown[]) => signalCheckMock(...args),
|
||||
signalRpcRequest: (...args: unknown[]) => signalRpcRequestMock(...args),
|
||||
}));
|
||||
|
||||
describe("probeSignal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("extracts version from {version} result", async () => {
|
||||
signalCheckMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
error: null,
|
||||
});
|
||||
signalRpcRequestMock.mockResolvedValueOnce({ version: "0.13.22" });
|
||||
|
||||
const res = await probeSignal("http://127.0.0.1:8080", 1000);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.version).toBe("0.13.22");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("returns ok=false when /check fails", async () => {
|
||||
signalCheckMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 503,
|
||||
error: "HTTP 503",
|
||||
});
|
||||
|
||||
const res = await probeSignal("http://127.0.0.1:8080", 1000);
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.status).toBe(503);
|
||||
expect(res.version).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,15 @@ export type SignalProbe = {
|
||||
version?: string | null;
|
||||
};
|
||||
|
||||
function parseSignalVersion(value: unknown): string | null {
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const version = (value as { version?: unknown }).version;
|
||||
if (typeof version === "string" && version.trim()) return version.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function probeSignal(
|
||||
baseUrl: string,
|
||||
timeoutMs: number,
|
||||
@@ -30,11 +39,11 @@ export async function probeSignal(
|
||||
};
|
||||
}
|
||||
try {
|
||||
const version = await signalRpcRequest<string>("version", undefined, {
|
||||
const version = await signalRpcRequest<unknown>("version", undefined, {
|
||||
baseUrl,
|
||||
timeoutMs,
|
||||
});
|
||||
result.version = typeof version === "string" ? version : null;
|
||||
result.version = parseSignalVersion(version);
|
||||
} catch (err) {
|
||||
result.error = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ function parseTarget(raw: string): SignalTarget {
|
||||
value = value.slice("signal:".length).trim();
|
||||
}
|
||||
if (lower.startsWith("username:")) {
|
||||
return { type: "username", username: value.slice("username:".length).trim() };
|
||||
return {
|
||||
type: "username",
|
||||
username: value.slice("username:".length).trim(),
|
||||
};
|
||||
}
|
||||
if (lower.startsWith("u:")) {
|
||||
return { type: "username", username: value.trim() };
|
||||
|
||||
Reference in New Issue
Block a user