@openclaw/signal 0.0.0 → 2026.6.11
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/README.md +12 -2
- package/dist/accounts-hOCHbEhX.js +40 -0
- package/dist/api.js +16 -0
- package/dist/approval-handler.runtime-CeJI1wow.js +158 -0
- package/dist/approval-resolver-BR0MioAA.js +15 -0
- package/dist/channel-LmY2UpOt.js +619 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-entry.js +18 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-B0-YpkC4.js +65 -0
- package/dist/config-api-KS-qhQvD.js +2 -0
- package/dist/config-schema-BiojLEsX.js +27 -0
- package/dist/contract-api.js +3 -0
- package/dist/identity-B6O4k8xg.js +130 -0
- package/dist/index.js +18 -0
- package/dist/install-signal-cli-CXgTF3de.js +243 -0
- package/dist/message-actions-Bue0g2Kc.js +548 -0
- package/dist/monitor-CUhIKHJo.js +1404 -0
- package/dist/probe-BQ_Izoya.js +48 -0
- package/dist/reaction-runtime-api-C_PQ45D9.js +1074 -0
- package/dist/reaction-runtime-api.js +2 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/runtime-api.js +18 -0
- package/dist/secret-contract-api.js +5 -0
- package/dist/send-CBlFUkY_.js +599 -0
- package/dist/send.runtime-CfaZd8X4.js +2 -0
- package/dist/setup-entry.js +11 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +22 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +760 -0
- package/node_modules/ws/lib/sender.js +607 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +562 -0
- package/node_modules/ws/lib/websocket.js +1407 -0
- package/node_modules/ws/package.json +70 -0
- package/node_modules/ws/wrapper.mjs +21 -0
- package/npm-shrinkwrap.json +36 -0
- package/openclaw.plugin.json +771 -0
- package/package.json +77 -7
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
import { i as resolveSignalAccount } from "./accounts-hOCHbEhX.js";
|
|
2
|
+
import { detectMime, parseMediaContentLength } from "openclaw/plugin-sdk/media-runtime";
|
|
3
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
+
import { asDateTimestampMs, parseStrictNonNegativeInteger, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
5
|
+
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime";
|
|
9
|
+
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
10
|
+
import WebSocket from "ws";
|
|
11
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import https from "node:https";
|
|
14
|
+
import { generateSecureUuid } from "openclaw/plugin-sdk/core";
|
|
15
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
16
|
+
//#region extensions/signal/src/client-container.ts
|
|
17
|
+
/**
|
|
18
|
+
* Signal client for bbernhard/signal-cli-rest-api container.
|
|
19
|
+
* Uses WebSocket for receiving messages and REST API for sending.
|
|
20
|
+
*
|
|
21
|
+
* This is a separate implementation from client.ts (native signal-cli)
|
|
22
|
+
* to keep the two modes cleanly isolated.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_TIMEOUT_MS$2 = 1e4;
|
|
25
|
+
const DEFAULT_ATTACHMENT_RESPONSE_MAX_BYTES = 1048576;
|
|
26
|
+
const CONTAINER_TEXT_STYLE_MARKERS = {
|
|
27
|
+
BOLD: "**",
|
|
28
|
+
ITALIC: "*",
|
|
29
|
+
STRIKETHROUGH: "~",
|
|
30
|
+
MONOSPACE: "`",
|
|
31
|
+
SPOILER: "||"
|
|
32
|
+
};
|
|
33
|
+
function normalizeBaseUrl$1(url) {
|
|
34
|
+
const trimmed = url.trim();
|
|
35
|
+
if (!trimmed) throw new Error("Signal base URL is required");
|
|
36
|
+
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
37
|
+
const parsed = new URL(withProtocol);
|
|
38
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Signal base URL unsupported protocol: ${parsed.protocol}`);
|
|
39
|
+
if (parsed.username || parsed.password) throw new Error("Signal base URL must not include credentials");
|
|
40
|
+
const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
|
|
41
|
+
return `${parsed.protocol}//${parsed.host}${pathname}`;
|
|
42
|
+
}
|
|
43
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
44
|
+
const fetchImpl = resolveFetch();
|
|
45
|
+
if (!fetchImpl) throw new Error("fetch is not available");
|
|
46
|
+
const safeTimeoutMs = resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS$2);
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timer = setTimeout(() => controller.abort(), safeTimeoutMs);
|
|
49
|
+
try {
|
|
50
|
+
return await fetchImpl(url, {
|
|
51
|
+
...init,
|
|
52
|
+
signal: controller.signal
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function normalizeMaxResponseBytes(value) {
|
|
59
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_ATTACHMENT_RESPONSE_MAX_BYTES;
|
|
60
|
+
return Math.floor(value);
|
|
61
|
+
}
|
|
62
|
+
function readContentLength(res) {
|
|
63
|
+
return parseMediaContentLength(res.headers?.get("content-length") ?? null) ?? void 0;
|
|
64
|
+
}
|
|
65
|
+
async function readCappedResponseBuffer(res, maxResponseBytes) {
|
|
66
|
+
const contentLength = readContentLength(res);
|
|
67
|
+
if (contentLength !== void 0 && contentLength > maxResponseBytes) throw new Error("Signal REST attachment exceeded size limit");
|
|
68
|
+
return await readResponseWithLimit(res, maxResponseBytes, { onOverflow: () => /* @__PURE__ */ new Error("Signal REST attachment exceeded size limit") });
|
|
69
|
+
}
|
|
70
|
+
async function releaseUnreadResponseBody(res) {
|
|
71
|
+
if (res?.bodyUsed !== true) await res?.body?.cancel().catch(() => void 0);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if bbernhard container REST API is available.
|
|
75
|
+
*/
|
|
76
|
+
async function containerCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS$2, account) {
|
|
77
|
+
const normalized = normalizeBaseUrl$1(baseUrl);
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = await fetchWithTimeout(`${normalized}/v1/about`, { method: "GET" }, timeoutMs);
|
|
81
|
+
if (!res.ok) return {
|
|
82
|
+
ok: false,
|
|
83
|
+
status: res.status,
|
|
84
|
+
error: `HTTP ${res.status}`
|
|
85
|
+
};
|
|
86
|
+
const receiveAccount = account?.trim();
|
|
87
|
+
if (receiveAccount) return await containerReceiveCheck(normalized, receiveAccount, timeoutMs);
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
status: res.status,
|
|
91
|
+
error: null
|
|
92
|
+
};
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
status: null,
|
|
97
|
+
error: err instanceof Error ? err.message : String(err)
|
|
98
|
+
};
|
|
99
|
+
} finally {
|
|
100
|
+
await releaseUnreadResponseBody(res);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function containerReceiveCheck(normalizedBaseUrl, account, timeoutMs) {
|
|
104
|
+
const wsUrl = `${normalizedBaseUrl.replace(/^http/, "ws")}/v1/receive/${encodeURIComponent(account)}`;
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const safeTimeoutMs = resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS$2);
|
|
107
|
+
let settled = false;
|
|
108
|
+
let ws;
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
settle({
|
|
111
|
+
ok: false,
|
|
112
|
+
status: null,
|
|
113
|
+
error: "Signal container receive WebSocket timed out"
|
|
114
|
+
});
|
|
115
|
+
ws?.terminate();
|
|
116
|
+
}, safeTimeoutMs);
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
const settle = (result) => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve(result);
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
ws = new WebSocket(wsUrl);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
settle({
|
|
128
|
+
ok: false,
|
|
129
|
+
status: null,
|
|
130
|
+
error: err instanceof Error ? err.message : String(err)
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
ws.once("open", () => {
|
|
135
|
+
settle({
|
|
136
|
+
ok: true,
|
|
137
|
+
status: 101,
|
|
138
|
+
error: null
|
|
139
|
+
});
|
|
140
|
+
ws?.close();
|
|
141
|
+
});
|
|
142
|
+
ws.once("unexpected-response", (_request, response) => {
|
|
143
|
+
settle({
|
|
144
|
+
ok: false,
|
|
145
|
+
status: response.statusCode ?? null,
|
|
146
|
+
error: `Signal container receive endpoint did not upgrade to WebSocket (HTTP ${response.statusCode ?? "unknown"})`
|
|
147
|
+
});
|
|
148
|
+
ws?.terminate();
|
|
149
|
+
});
|
|
150
|
+
ws.once("error", (err) => {
|
|
151
|
+
settle({
|
|
152
|
+
ok: false,
|
|
153
|
+
status: null,
|
|
154
|
+
error: err instanceof Error ? err.message : String(err)
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
ws.once("close", (code, reason) => {
|
|
158
|
+
settle({
|
|
159
|
+
ok: false,
|
|
160
|
+
status: null,
|
|
161
|
+
error: `Signal container receive WebSocket closed before open (${code}${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""})`
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Make a REST API request to bbernhard container.
|
|
168
|
+
*/
|
|
169
|
+
async function containerRestRequest(endpoint, opts, method = "GET", body) {
|
|
170
|
+
const url = `${normalizeBaseUrl$1(opts.baseUrl)}${endpoint}`;
|
|
171
|
+
const init = {
|
|
172
|
+
method,
|
|
173
|
+
headers: { "Content-Type": "application/json" }
|
|
174
|
+
};
|
|
175
|
+
if (body) init.body = JSON.stringify(body);
|
|
176
|
+
const res = await fetchWithTimeout(url, init, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$2);
|
|
177
|
+
if (res.status === 204) return;
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
const errorText = await res.text().catch(() => "");
|
|
180
|
+
throw new Error(`Signal REST ${res.status}: ${errorText || res.statusText}`);
|
|
181
|
+
}
|
|
182
|
+
const text = await res.text();
|
|
183
|
+
if (!text) return;
|
|
184
|
+
return JSON.parse(text);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Fetch attachment binary from bbernhard container.
|
|
188
|
+
*/
|
|
189
|
+
async function containerFetchAttachment(attachmentId, opts) {
|
|
190
|
+
const url = `${normalizeBaseUrl$1(opts.baseUrl)}/v1/attachments/${encodeURIComponent(attachmentId)}`;
|
|
191
|
+
let res;
|
|
192
|
+
try {
|
|
193
|
+
res = await fetchWithTimeout(url, { method: "GET" }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$2);
|
|
194
|
+
if (!res.ok) return null;
|
|
195
|
+
return await readCappedResponseBuffer(res, normalizeMaxResponseBytes(opts.maxResponseBytes));
|
|
196
|
+
} finally {
|
|
197
|
+
await releaseUnreadResponseBody(res);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Stream messages using WebSocket from bbernhard container.
|
|
202
|
+
* The Promise resolves when the connection closes (for any reason).
|
|
203
|
+
* The caller (runSignalLoopAdapter) is responsible for reconnection.
|
|
204
|
+
*/
|
|
205
|
+
async function streamContainerEvents(params) {
|
|
206
|
+
const normalized = normalizeBaseUrl$1(params.baseUrl);
|
|
207
|
+
const wsUrl = `${normalized.replace(/^http/, "ws")}/v1/receive/${encodeURIComponent(params.account ?? "")}`;
|
|
208
|
+
const redactedWsUrl = `${normalized.replace(/^http/, "ws")}/v1/receive/<redacted>`;
|
|
209
|
+
const log = params.logger?.log ?? (() => {});
|
|
210
|
+
const logError = params.logger?.error ?? (() => {});
|
|
211
|
+
log(`[signal-ws] connecting to ${redactedWsUrl}`);
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
let ws;
|
|
214
|
+
let resolved = false;
|
|
215
|
+
let abortHandler;
|
|
216
|
+
const cleanup = () => {
|
|
217
|
+
if (resolved) return;
|
|
218
|
+
resolved = true;
|
|
219
|
+
if (abortHandler) {
|
|
220
|
+
params.abortSignal?.removeEventListener("abort", abortHandler);
|
|
221
|
+
abortHandler = void 0;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
ws = new WebSocket(wsUrl);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logError(`[signal-ws] failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
reject(toLintErrorObject$1(err, "Non-Error rejection"));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
ws.on("open", () => {
|
|
232
|
+
log("[signal-ws] connected");
|
|
233
|
+
});
|
|
234
|
+
ws.on("message", (data) => {
|
|
235
|
+
try {
|
|
236
|
+
const text = data.toString();
|
|
237
|
+
const envelope = JSON.parse(text);
|
|
238
|
+
if (envelope) params.onEvent(envelope);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
logError(`[signal-ws] parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
ws.on("error", (err) => {
|
|
244
|
+
logError(`[signal-ws] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
|
+
});
|
|
246
|
+
ws.on("close", (code, reason) => {
|
|
247
|
+
log(`[signal-ws] closed (code=${code}, reason=${reason?.toString() || "no reason"})`);
|
|
248
|
+
cleanup();
|
|
249
|
+
resolve();
|
|
250
|
+
});
|
|
251
|
+
ws.on("ping", () => {
|
|
252
|
+
log("[signal-ws] ping received");
|
|
253
|
+
});
|
|
254
|
+
ws.on("pong", () => {
|
|
255
|
+
log("[signal-ws] pong received");
|
|
256
|
+
});
|
|
257
|
+
if (params.abortSignal) {
|
|
258
|
+
abortHandler = () => {
|
|
259
|
+
log("[signal-ws] aborted, closing connection");
|
|
260
|
+
cleanup();
|
|
261
|
+
ws.close();
|
|
262
|
+
resolve();
|
|
263
|
+
};
|
|
264
|
+
params.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Convert local file paths to base64 data URIs for the container REST API.
|
|
270
|
+
* The bbernhard container /v2/send only accepts `base64_attachments` (not file paths).
|
|
271
|
+
*/
|
|
272
|
+
async function filesToBase64DataUris(filePaths) {
|
|
273
|
+
const results = [];
|
|
274
|
+
for (const filePath of filePaths) {
|
|
275
|
+
const buffer = await fs.readFile(filePath);
|
|
276
|
+
const mime = await detectMime({
|
|
277
|
+
buffer,
|
|
278
|
+
filePath
|
|
279
|
+
}) ?? "application/octet-stream";
|
|
280
|
+
const filename = path.basename(filePath);
|
|
281
|
+
const b64 = buffer.toString("base64");
|
|
282
|
+
results.push(`data:${mime};filename=${filename};base64,${b64}`);
|
|
283
|
+
}
|
|
284
|
+
return results;
|
|
285
|
+
}
|
|
286
|
+
function escapeContainerStyledText(text) {
|
|
287
|
+
return text.replace(/[*~`|]/g, (char) => `\\${char}`);
|
|
288
|
+
}
|
|
289
|
+
function renderContainerStyledText(text, styles) {
|
|
290
|
+
const spans = styles.map((style) => {
|
|
291
|
+
const marker = CONTAINER_TEXT_STYLE_MARKERS[style.style];
|
|
292
|
+
if (!marker) return null;
|
|
293
|
+
const start = Math.max(0, Math.min(style.start, text.length));
|
|
294
|
+
const end = Math.max(start, Math.min(style.start + style.length, text.length));
|
|
295
|
+
if (end <= start) return null;
|
|
296
|
+
return {
|
|
297
|
+
start,
|
|
298
|
+
end,
|
|
299
|
+
marker
|
|
300
|
+
};
|
|
301
|
+
}).filter((span) => span !== null);
|
|
302
|
+
if (spans.length === 0) return text;
|
|
303
|
+
const positions = [...new Set([
|
|
304
|
+
0,
|
|
305
|
+
text.length,
|
|
306
|
+
...spans.flatMap((span) => [span.start, span.end])
|
|
307
|
+
])].toSorted((a, b) => a - b);
|
|
308
|
+
let rendered = "";
|
|
309
|
+
for (let i = 0; i < positions.length; i += 1) {
|
|
310
|
+
const pos = positions[i];
|
|
311
|
+
for (const span of spans.filter((candidate) => candidate.end === pos).toSorted((a, b) => b.start - a.start)) rendered += span.marker;
|
|
312
|
+
for (const span of spans.filter((candidate) => candidate.start === pos).toSorted((a, b) => b.end - a.end)) rendered += span.marker;
|
|
313
|
+
const next = positions[i + 1];
|
|
314
|
+
if (next !== void 0 && next > pos) rendered += escapeContainerStyledText(text.slice(pos, next));
|
|
315
|
+
}
|
|
316
|
+
return rendered;
|
|
317
|
+
}
|
|
318
|
+
function parseContainerSendTimestamp(raw) {
|
|
319
|
+
if (raw == null) return;
|
|
320
|
+
const timestamp = parseStrictNonNegativeInteger(raw);
|
|
321
|
+
if (timestamp === void 0) throw new Error("Signal REST send returned invalid timestamp");
|
|
322
|
+
return timestamp;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Send message via bbernhard container REST API.
|
|
326
|
+
*/
|
|
327
|
+
async function containerSendMessage(params) {
|
|
328
|
+
const payload = {
|
|
329
|
+
message: params.message,
|
|
330
|
+
number: params.account,
|
|
331
|
+
recipients: params.recipients
|
|
332
|
+
};
|
|
333
|
+
if (params.textStyles && params.textStyles.length > 0) {
|
|
334
|
+
payload.message = renderContainerStyledText(params.message, params.textStyles);
|
|
335
|
+
payload["text_mode"] = "styled";
|
|
336
|
+
}
|
|
337
|
+
if (params.attachments && params.attachments.length > 0) payload.base64_attachments = await filesToBase64DataUris(params.attachments);
|
|
338
|
+
const timestamp = parseContainerSendTimestamp((await containerRestRequest("/v2/send", {
|
|
339
|
+
baseUrl: params.baseUrl,
|
|
340
|
+
timeoutMs: params.timeoutMs
|
|
341
|
+
}, "POST", payload))?.timestamp);
|
|
342
|
+
return timestamp === void 0 ? {} : { timestamp };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Send typing indicator via bbernhard container REST API.
|
|
346
|
+
*/
|
|
347
|
+
async function containerSendTyping(params) {
|
|
348
|
+
const method = params.stop ? "DELETE" : "PUT";
|
|
349
|
+
await containerRestRequest(`/v1/typing-indicator/${encodeURIComponent(params.account)}`, {
|
|
350
|
+
baseUrl: params.baseUrl,
|
|
351
|
+
timeoutMs: params.timeoutMs
|
|
352
|
+
}, method, { recipient: params.recipient });
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Send read receipt via bbernhard container REST API.
|
|
357
|
+
*/
|
|
358
|
+
async function containerSendReceipt(params) {
|
|
359
|
+
await containerRestRequest(`/v1/receipts/${encodeURIComponent(params.account)}`, {
|
|
360
|
+
baseUrl: params.baseUrl,
|
|
361
|
+
timeoutMs: params.timeoutMs
|
|
362
|
+
}, "POST", {
|
|
363
|
+
recipient: params.recipient,
|
|
364
|
+
timestamp: params.timestamp,
|
|
365
|
+
receipt_type: params.type ?? "read"
|
|
366
|
+
});
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Send a reaction to a message via bbernhard container REST API.
|
|
371
|
+
*/
|
|
372
|
+
async function containerSendReaction(params) {
|
|
373
|
+
const payload = {
|
|
374
|
+
recipient: params.recipient,
|
|
375
|
+
reaction: params.emoji,
|
|
376
|
+
target_author: params.targetAuthor,
|
|
377
|
+
timestamp: params.targetTimestamp
|
|
378
|
+
};
|
|
379
|
+
if (params.groupId) payload.group_id = params.groupId;
|
|
380
|
+
return await containerRestRequest(`/v1/reactions/${encodeURIComponent(params.account)}`, {
|
|
381
|
+
baseUrl: params.baseUrl,
|
|
382
|
+
timeoutMs: params.timeoutMs
|
|
383
|
+
}, "POST", payload) ?? {};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Remove a reaction from a message via bbernhard container REST API.
|
|
387
|
+
*/
|
|
388
|
+
async function containerRemoveReaction(params) {
|
|
389
|
+
const payload = {
|
|
390
|
+
recipient: params.recipient,
|
|
391
|
+
reaction: params.emoji,
|
|
392
|
+
target_author: params.targetAuthor,
|
|
393
|
+
timestamp: params.targetTimestamp
|
|
394
|
+
};
|
|
395
|
+
if (params.groupId) payload.group_id = params.groupId;
|
|
396
|
+
return await containerRestRequest(`/v1/reactions/${encodeURIComponent(params.account)}`, {
|
|
397
|
+
baseUrl: params.baseUrl,
|
|
398
|
+
timeoutMs: params.timeoutMs
|
|
399
|
+
}, "DELETE", payload) ?? {};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Strip the "uuid:" prefix that native signal-cli accepts but the container API rejects.
|
|
403
|
+
*/
|
|
404
|
+
function stripUuidPrefix(id) {
|
|
405
|
+
return id.startsWith("uuid:") ? id.slice(5) : id;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Convert a group internal_id to the container-expected format.
|
|
409
|
+
* The bbernhard container expects groups as "group.{base64(internal_id)}".
|
|
410
|
+
*/
|
|
411
|
+
function formatGroupIdForContainer(groupId) {
|
|
412
|
+
if (groupId.startsWith("group.")) return groupId;
|
|
413
|
+
return `group.${Buffer.from(groupId).toString("base64")}`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Drop-in replacement for native signalRpcRequest that translates
|
|
417
|
+
* JSON-RPC method + params into the equivalent container REST API calls.
|
|
418
|
+
* This keeps all container protocol details (uuid: stripping, group ID
|
|
419
|
+
* formatting, base64 attachments, text-style conversion) isolated here.
|
|
420
|
+
*/
|
|
421
|
+
async function containerRpcRequest(method, params, opts) {
|
|
422
|
+
const p = params ?? {};
|
|
423
|
+
switch (method) {
|
|
424
|
+
case "send": {
|
|
425
|
+
const recipients = (p.recipient ?? []).map(stripUuidPrefix);
|
|
426
|
+
const usernames = (p.username ?? []).map(stripUuidPrefix);
|
|
427
|
+
const groupId = p.groupId;
|
|
428
|
+
const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : void 0;
|
|
429
|
+
const finalRecipients = recipients.length > 0 ? recipients : usernames.length > 0 ? usernames : formattedGroupId ? [formattedGroupId] : [];
|
|
430
|
+
const textStyles = p["text-style"]?.map((s) => {
|
|
431
|
+
const [start, length, style] = s.split(":");
|
|
432
|
+
return {
|
|
433
|
+
start: Number(start),
|
|
434
|
+
length: Number(length),
|
|
435
|
+
style
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
return await containerSendMessage({
|
|
439
|
+
baseUrl: opts.baseUrl,
|
|
440
|
+
account: p.account ?? "",
|
|
441
|
+
recipients: finalRecipients,
|
|
442
|
+
message: p.message ?? "",
|
|
443
|
+
textStyles,
|
|
444
|
+
attachments: p.attachments,
|
|
445
|
+
timeoutMs: opts.timeoutMs
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
case "sendTyping": {
|
|
449
|
+
const recipient = stripUuidPrefix(p.recipient?.[0] ?? (p.groupId ? formatGroupIdForContainer(p.groupId) : ""));
|
|
450
|
+
await containerSendTyping({
|
|
451
|
+
baseUrl: opts.baseUrl,
|
|
452
|
+
account: p.account ?? "",
|
|
453
|
+
recipient,
|
|
454
|
+
stop: p.stop,
|
|
455
|
+
timeoutMs: opts.timeoutMs
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
case "sendReceipt": {
|
|
460
|
+
const recipient = stripUuidPrefix(p.recipient?.[0] ?? "");
|
|
461
|
+
await containerSendReceipt({
|
|
462
|
+
baseUrl: opts.baseUrl,
|
|
463
|
+
account: p.account ?? "",
|
|
464
|
+
recipient,
|
|
465
|
+
timestamp: p.targetTimestamp,
|
|
466
|
+
type: p.type,
|
|
467
|
+
timeoutMs: opts.timeoutMs
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
case "sendReaction": {
|
|
472
|
+
const recipient = stripUuidPrefix(p.recipients?.[0] ?? "");
|
|
473
|
+
const groupId = p.groupIds?.[0] ?? void 0;
|
|
474
|
+
const formattedGroupId = groupId ? formatGroupIdForContainer(groupId) : void 0;
|
|
475
|
+
const effectiveRecipient = formattedGroupId || recipient || "";
|
|
476
|
+
const reactionParams = {
|
|
477
|
+
baseUrl: opts.baseUrl,
|
|
478
|
+
account: p.account ?? "",
|
|
479
|
+
recipient: effectiveRecipient,
|
|
480
|
+
emoji: p.emoji ?? "",
|
|
481
|
+
targetAuthor: stripUuidPrefix(p.targetAuthor ?? recipient),
|
|
482
|
+
targetTimestamp: p.targetTimestamp,
|
|
483
|
+
groupId: formattedGroupId,
|
|
484
|
+
timeoutMs: opts.timeoutMs
|
|
485
|
+
};
|
|
486
|
+
return await (p.remove ? containerRemoveReaction : containerSendReaction)(reactionParams);
|
|
487
|
+
}
|
|
488
|
+
case "getAttachment": {
|
|
489
|
+
const attachmentId = p.id;
|
|
490
|
+
const buffer = await containerFetchAttachment(attachmentId, {
|
|
491
|
+
baseUrl: opts.baseUrl,
|
|
492
|
+
timeoutMs: opts.timeoutMs,
|
|
493
|
+
maxResponseBytes: opts.maxResponseBytes
|
|
494
|
+
});
|
|
495
|
+
if (!buffer) return { data: void 0 };
|
|
496
|
+
return { data: buffer.toString("base64") };
|
|
497
|
+
}
|
|
498
|
+
case "version": return await containerRestRequest("/v1/about", {
|
|
499
|
+
baseUrl: opts.baseUrl,
|
|
500
|
+
timeoutMs: opts.timeoutMs
|
|
501
|
+
});
|
|
502
|
+
default: throw new Error(`Unsupported container RPC method: ${method}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function toLintErrorObject$1(value, fallbackMessage) {
|
|
506
|
+
if (value instanceof Error) return value;
|
|
507
|
+
if (typeof value === "string") return new Error(value);
|
|
508
|
+
const error = new Error(fallbackMessage, { cause: value });
|
|
509
|
+
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
|
|
510
|
+
return error;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region extensions/signal/src/client.ts
|
|
514
|
+
const DEFAULT_TIMEOUT_MS$1 = 1e4;
|
|
515
|
+
const DEFAULT_SIGNAL_HTTP_RESPONSE_MAX_BYTES = 1048576;
|
|
516
|
+
const MAX_SIGNAL_SSE_BUFFER_BYTES = 1048576;
|
|
517
|
+
const MAX_SIGNAL_SSE_EVENT_DATA_BYTES = 1048576;
|
|
518
|
+
function createSignalSseAbortError() {
|
|
519
|
+
const error = /* @__PURE__ */ new Error("Signal SSE aborted");
|
|
520
|
+
error.name = "AbortError";
|
|
521
|
+
return error;
|
|
522
|
+
}
|
|
523
|
+
function normalizeBaseUrl(url) {
|
|
524
|
+
const trimmed = url.trim();
|
|
525
|
+
if (!trimmed) throw new Error("Signal base URL is required");
|
|
526
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed.replace(/\/+$/, "");
|
|
527
|
+
return `http://${trimmed}`.replace(/\/+$/, "");
|
|
528
|
+
}
|
|
529
|
+
function parseSignalBaseUrl(url) {
|
|
530
|
+
const parsed = new URL(normalizeBaseUrl(url));
|
|
531
|
+
if (parsed.username || parsed.password) throw new Error("Signal base URL must not include credentials");
|
|
532
|
+
return parsed;
|
|
533
|
+
}
|
|
534
|
+
function resolveSignalEndpointUrl(baseUrl, pathname) {
|
|
535
|
+
return new URL(pathname, parseSignalBaseUrl(baseUrl));
|
|
536
|
+
}
|
|
537
|
+
function parseSignalRpcResponse(text, status) {
|
|
538
|
+
let parsed;
|
|
539
|
+
try {
|
|
540
|
+
parsed = JSON.parse(text);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
throw new Error(`Signal RPC returned malformed JSON (status ${status})`, { cause: err });
|
|
543
|
+
}
|
|
544
|
+
if (!parsed || typeof parsed !== "object") throw new Error(`Signal RPC returned invalid response envelope (status ${status})`);
|
|
545
|
+
const rpc = parsed;
|
|
546
|
+
const hasResult = Object.hasOwn(rpc, "result");
|
|
547
|
+
if (!rpc.error && !hasResult) throw new Error(`Signal RPC returned invalid response envelope (status ${status})`);
|
|
548
|
+
return rpc;
|
|
549
|
+
}
|
|
550
|
+
function assertSignalHttpProtocol(url, label) {
|
|
551
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Signal ${label} unsupported protocol: ${url.protocol}`);
|
|
552
|
+
}
|
|
553
|
+
function normalizeSignalHttpResponseMaxBytes(value) {
|
|
554
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_SIGNAL_HTTP_RESPONSE_MAX_BYTES;
|
|
555
|
+
return Math.floor(value);
|
|
556
|
+
}
|
|
557
|
+
function normalizeSignalSseTimeoutMs(timeoutMs) {
|
|
558
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return null;
|
|
559
|
+
return resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS$1);
|
|
560
|
+
}
|
|
561
|
+
function requestSignalHttpText(url, options) {
|
|
562
|
+
assertSignalHttpProtocol(url, "HTTP");
|
|
563
|
+
const timeoutMs = resolveTimerTimeoutMs(options.timeoutMs, DEFAULT_TIMEOUT_MS$1);
|
|
564
|
+
const client = url.protocol === "https:" ? https : http;
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
let settled = false;
|
|
567
|
+
const deadline = setTimeout(() => {
|
|
568
|
+
request?.destroy(/* @__PURE__ */ new Error(`Signal HTTP exceeded deadline after ${timeoutMs}ms`));
|
|
569
|
+
}, timeoutMs);
|
|
570
|
+
deadline.unref?.();
|
|
571
|
+
const cleanup = () => {
|
|
572
|
+
clearTimeout(deadline);
|
|
573
|
+
request?.setTimeout(0);
|
|
574
|
+
};
|
|
575
|
+
const rejectOnce = (error) => {
|
|
576
|
+
if (settled) return;
|
|
577
|
+
settled = true;
|
|
578
|
+
cleanup();
|
|
579
|
+
reject(toLintErrorObject(error, "Non-Error rejection"));
|
|
580
|
+
};
|
|
581
|
+
const resolveOnce = (response) => {
|
|
582
|
+
if (settled) return;
|
|
583
|
+
settled = true;
|
|
584
|
+
cleanup();
|
|
585
|
+
resolve(response);
|
|
586
|
+
};
|
|
587
|
+
const maxResponseBytes = normalizeSignalHttpResponseMaxBytes(options.maxResponseBytes);
|
|
588
|
+
const request = client.request(url, {
|
|
589
|
+
method: options.method,
|
|
590
|
+
headers: options.headers
|
|
591
|
+
}, (res) => {
|
|
592
|
+
const chunks = [];
|
|
593
|
+
let totalBytes = 0;
|
|
594
|
+
res.on("data", (chunk) => {
|
|
595
|
+
const next = typeof chunk === "string" ? Buffer$1.from(chunk) : chunk;
|
|
596
|
+
totalBytes += next.byteLength;
|
|
597
|
+
if (totalBytes > maxResponseBytes) {
|
|
598
|
+
const error = /* @__PURE__ */ new Error("Signal HTTP response exceeded size limit");
|
|
599
|
+
request?.destroy(error);
|
|
600
|
+
res.destroy(error);
|
|
601
|
+
rejectOnce(error);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
chunks.push(next);
|
|
605
|
+
});
|
|
606
|
+
res.on("error", rejectOnce);
|
|
607
|
+
res.on("end", () => {
|
|
608
|
+
resolveOnce({
|
|
609
|
+
status: res.statusCode ?? 0,
|
|
610
|
+
statusText: res.statusMessage || "error",
|
|
611
|
+
text: Buffer$1.concat(chunks).toString("utf8")
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
request.setTimeout(timeoutMs, () => {
|
|
616
|
+
request?.destroy(/* @__PURE__ */ new Error(`Signal HTTP timed out after ${timeoutMs}ms`));
|
|
617
|
+
});
|
|
618
|
+
request.on("error", rejectOnce);
|
|
619
|
+
if (options.body !== void 0) request.write(options.body);
|
|
620
|
+
request.end();
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async function signalRpcRequest$1(method, params, opts) {
|
|
624
|
+
const id = generateSecureUuid();
|
|
625
|
+
const body = JSON.stringify({
|
|
626
|
+
jsonrpc: "2.0",
|
|
627
|
+
method,
|
|
628
|
+
params,
|
|
629
|
+
id
|
|
630
|
+
});
|
|
631
|
+
const res = await requestSignalHttpText(resolveSignalEndpointUrl(opts.baseUrl, "/api/v1/rpc"), {
|
|
632
|
+
method: "POST",
|
|
633
|
+
headers: {
|
|
634
|
+
"Content-Type": "application/json",
|
|
635
|
+
"Content-Length": String(Buffer$1.byteLength(body))
|
|
636
|
+
},
|
|
637
|
+
body,
|
|
638
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$1,
|
|
639
|
+
maxResponseBytes: opts.maxResponseBytes
|
|
640
|
+
});
|
|
641
|
+
if (res.status === 201) return;
|
|
642
|
+
if (!res.text) throw new Error(`Signal RPC empty response (status ${res.status})`);
|
|
643
|
+
const parsed = parseSignalRpcResponse(res.text, res.status);
|
|
644
|
+
if (parsed.error) {
|
|
645
|
+
const code = parsed.error.code ?? "unknown";
|
|
646
|
+
const msg = parsed.error.message ?? "Signal RPC error";
|
|
647
|
+
throw new Error(`Signal RPC ${code}: ${msg}`);
|
|
648
|
+
}
|
|
649
|
+
return parsed.result;
|
|
650
|
+
}
|
|
651
|
+
async function signalCheck$1(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
|
|
652
|
+
try {
|
|
653
|
+
const res = await requestSignalHttpText(resolveSignalEndpointUrl(baseUrl, "/api/v1/check"), {
|
|
654
|
+
method: "GET",
|
|
655
|
+
timeoutMs
|
|
656
|
+
});
|
|
657
|
+
if (res.status < 200 || res.status >= 300) return {
|
|
658
|
+
ok: false,
|
|
659
|
+
status: res.status,
|
|
660
|
+
error: `HTTP ${res.status}`
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
ok: true,
|
|
664
|
+
status: res.status,
|
|
665
|
+
error: null
|
|
666
|
+
};
|
|
667
|
+
} catch (err) {
|
|
668
|
+
return {
|
|
669
|
+
ok: false,
|
|
670
|
+
status: null,
|
|
671
|
+
error: formatErrorMessage(err)
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function openSignalEventStream(url, abortSignal, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
|
|
676
|
+
assertSignalHttpProtocol(url, "SSE");
|
|
677
|
+
if (abortSignal?.aborted) throw createSignalSseAbortError();
|
|
678
|
+
const client = url.protocol === "https:" ? https : http;
|
|
679
|
+
return new Promise((resolve, reject) => {
|
|
680
|
+
let settled = false;
|
|
681
|
+
let response;
|
|
682
|
+
let onAbort = () => {};
|
|
683
|
+
const effectiveTimeoutMs = normalizeSignalSseTimeoutMs(timeoutMs);
|
|
684
|
+
const headerDeadline = effectiveTimeoutMs === null ? void 0 : setTimeout(() => {
|
|
685
|
+
const error = /* @__PURE__ */ new Error(`Signal SSE connection timed out after ${effectiveTimeoutMs}ms`);
|
|
686
|
+
response?.destroy(error);
|
|
687
|
+
request.destroy(error);
|
|
688
|
+
rejectOnce(error);
|
|
689
|
+
}, effectiveTimeoutMs);
|
|
690
|
+
headerDeadline?.unref?.();
|
|
691
|
+
const cleanup = () => {
|
|
692
|
+
if (headerDeadline) clearTimeout(headerDeadline);
|
|
693
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
694
|
+
};
|
|
695
|
+
const rejectOnce = (error) => {
|
|
696
|
+
if (settled) return;
|
|
697
|
+
settled = true;
|
|
698
|
+
cleanup();
|
|
699
|
+
reject(toLintErrorObject(error, "Non-Error rejection"));
|
|
700
|
+
};
|
|
701
|
+
const request = client.request(url, {
|
|
702
|
+
method: "GET",
|
|
703
|
+
headers: { Accept: "text/event-stream" }
|
|
704
|
+
}, (res) => {
|
|
705
|
+
const status = res.statusCode ?? 0;
|
|
706
|
+
if (status < 200 || status >= 300) {
|
|
707
|
+
res.resume();
|
|
708
|
+
rejectOnce(/* @__PURE__ */ new Error(`Signal SSE failed (${status} ${res.statusMessage || "error"})`));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (settled) {
|
|
712
|
+
res.destroy();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (headerDeadline) clearTimeout(headerDeadline);
|
|
716
|
+
settled = true;
|
|
717
|
+
response = res;
|
|
718
|
+
resolve({
|
|
719
|
+
response: res,
|
|
720
|
+
cleanup
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
onAbort = () => {
|
|
724
|
+
const error = createSignalSseAbortError();
|
|
725
|
+
response?.destroy(error);
|
|
726
|
+
request.destroy(error);
|
|
727
|
+
rejectOnce(error);
|
|
728
|
+
};
|
|
729
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
730
|
+
request.on("error", rejectOnce);
|
|
731
|
+
request.end();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
async function streamSignalEvents$1(params) {
|
|
735
|
+
const url = resolveSignalEndpointUrl(params.baseUrl, "/api/v1/events");
|
|
736
|
+
if (params.account) url.searchParams.set("account", params.account);
|
|
737
|
+
const { response, cleanup } = await openSignalEventStream(url, params.abortSignal, params.timeoutMs ?? DEFAULT_TIMEOUT_MS$1);
|
|
738
|
+
const decoder = new TextDecoder();
|
|
739
|
+
let buffer = "";
|
|
740
|
+
let bufferedBytes = 0;
|
|
741
|
+
let currentEvent = {};
|
|
742
|
+
let currentEventDataBytes = 0;
|
|
743
|
+
const flushEvent = () => {
|
|
744
|
+
if (!currentEvent.data && !currentEvent.event && !currentEvent.id) return;
|
|
745
|
+
params.onEvent({
|
|
746
|
+
event: currentEvent.event,
|
|
747
|
+
data: currentEvent.data,
|
|
748
|
+
id: currentEvent.id
|
|
749
|
+
});
|
|
750
|
+
currentEvent = {};
|
|
751
|
+
currentEventDataBytes = 0;
|
|
752
|
+
};
|
|
753
|
+
const processLine = (line) => {
|
|
754
|
+
if (line === "") {
|
|
755
|
+
flushEvent();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (line.startsWith(":")) return;
|
|
759
|
+
const [rawField, ...rest] = line.split(":");
|
|
760
|
+
const field = rawField.trim();
|
|
761
|
+
const rawValue = rest.join(":");
|
|
762
|
+
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
763
|
+
if (field === "event") currentEvent.event = value;
|
|
764
|
+
else if (field === "data") {
|
|
765
|
+
const segment = currentEvent.data ? `\n${value}` : value;
|
|
766
|
+
currentEventDataBytes += Buffer$1.byteLength(segment, "utf8");
|
|
767
|
+
if (currentEventDataBytes > MAX_SIGNAL_SSE_EVENT_DATA_BYTES) throw new Error("Signal SSE event data exceeded size limit");
|
|
768
|
+
currentEvent.data = currentEvent.data ? `${currentEvent.data}${segment}` : segment;
|
|
769
|
+
} else if (field === "id") currentEvent.id = value;
|
|
770
|
+
};
|
|
771
|
+
const drainCompleteLines = () => {
|
|
772
|
+
let lineEnd = buffer.indexOf("\n");
|
|
773
|
+
while (lineEnd !== -1) {
|
|
774
|
+
let line = buffer.slice(0, lineEnd);
|
|
775
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
776
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
777
|
+
processLine(line);
|
|
778
|
+
lineEnd = buffer.indexOf("\n");
|
|
779
|
+
}
|
|
780
|
+
bufferedBytes = Buffer$1.byteLength(buffer, "utf8");
|
|
781
|
+
};
|
|
782
|
+
try {
|
|
783
|
+
for await (const chunk of response) {
|
|
784
|
+
const value = typeof chunk === "string" ? Buffer$1.from(chunk) : chunk;
|
|
785
|
+
bufferedBytes += value.byteLength;
|
|
786
|
+
if (bufferedBytes > MAX_SIGNAL_SSE_BUFFER_BYTES) throw new Error("Signal SSE buffer exceeded size limit");
|
|
787
|
+
buffer += decoder.decode(value, { stream: true });
|
|
788
|
+
drainCompleteLines();
|
|
789
|
+
}
|
|
790
|
+
const tail = decoder.decode();
|
|
791
|
+
if (tail) {
|
|
792
|
+
buffer += tail;
|
|
793
|
+
bufferedBytes = Buffer$1.byteLength(buffer, "utf8");
|
|
794
|
+
}
|
|
795
|
+
if (bufferedBytes > MAX_SIGNAL_SSE_BUFFER_BYTES) throw new Error("Signal SSE buffer exceeded size limit");
|
|
796
|
+
drainCompleteLines();
|
|
797
|
+
} finally {
|
|
798
|
+
cleanup();
|
|
799
|
+
}
|
|
800
|
+
flushEvent();
|
|
801
|
+
}
|
|
802
|
+
function toLintErrorObject(value, fallbackMessage) {
|
|
803
|
+
if (value instanceof Error) return value;
|
|
804
|
+
if (typeof value === "string") return new Error(value);
|
|
805
|
+
const error = new Error(fallbackMessage, { cause: value });
|
|
806
|
+
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
|
|
807
|
+
return error;
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region extensions/signal/src/client-adapter.ts
|
|
811
|
+
/**
|
|
812
|
+
* Signal client adapter - unified interface for both native signal-cli and bbernhard container.
|
|
813
|
+
*
|
|
814
|
+
* This adapter provides a single API that routes to the appropriate implementation
|
|
815
|
+
* based on the configured API mode. Exports mirror client.ts names so consumers
|
|
816
|
+
* only need to change their import path.
|
|
817
|
+
*/
|
|
818
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
819
|
+
const MODE_CACHE_TTL_MS = 3e4;
|
|
820
|
+
const NATIVE_PREFERENCE_GRACE_MS = 50;
|
|
821
|
+
const detectedModeCache = /* @__PURE__ */ new Map();
|
|
822
|
+
function resolveConfiguredApiMode(configured) {
|
|
823
|
+
if (configured === "native" || configured === "container") return configured;
|
|
824
|
+
return "auto";
|
|
825
|
+
}
|
|
826
|
+
function formatErrorMessage$1(error) {
|
|
827
|
+
return error instanceof Error ? error.message : String(error);
|
|
828
|
+
}
|
|
829
|
+
function resolveAutoProbeTimeoutMs(timeoutMs) {
|
|
830
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
831
|
+
}
|
|
832
|
+
function waitForNativePreferenceGrace(nativeResultPromise) {
|
|
833
|
+
return new Promise((resolve) => {
|
|
834
|
+
const timer = setTimeout(() => resolve({ ok: false }), NATIVE_PREFERENCE_GRACE_MS);
|
|
835
|
+
timer.unref?.();
|
|
836
|
+
nativeResultPromise.then((result) => {
|
|
837
|
+
clearTimeout(timer);
|
|
838
|
+
resolve(result);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
async function resolveAutoApiMode(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
|
|
843
|
+
const rawNow = Date.now();
|
|
844
|
+
const now = asDateTimestampMs(rawNow);
|
|
845
|
+
const cached = detectedModeCache.get(baseUrl);
|
|
846
|
+
if (cached) if (now !== void 0 && cached.expiresAt > now) {
|
|
847
|
+
if (cached.mode !== "container" || !options.requireContainerReceive || Boolean(options.account?.trim()) && cached.receiveAccount === options.account?.trim()) return cached.mode;
|
|
848
|
+
} else detectedModeCache.delete(baseUrl);
|
|
849
|
+
const detected = await detectSignalApiMode(baseUrl, timeoutMs, options);
|
|
850
|
+
const expiresAt = resolveExpiresAtMsFromDurationMs(MODE_CACHE_TTL_MS, { nowMs: rawNow });
|
|
851
|
+
if (expiresAt !== void 0) detectedModeCache.set(baseUrl, {
|
|
852
|
+
mode: detected,
|
|
853
|
+
expiresAt,
|
|
854
|
+
...detected === "container" && options.requireContainerReceive && options.account ? { receiveAccount: options.account } : {}
|
|
855
|
+
});
|
|
856
|
+
return detected;
|
|
857
|
+
}
|
|
858
|
+
async function resolveApiModeForOperation(params) {
|
|
859
|
+
const configured = resolveConfiguredApiMode(params.apiMode);
|
|
860
|
+
if (configured === "native" || configured === "container") return configured;
|
|
861
|
+
return resolveAutoApiMode(params.baseUrl, params.timeoutMs ?? DEFAULT_TIMEOUT_MS, {
|
|
862
|
+
account: params.account,
|
|
863
|
+
requireContainerReceive: params.requireContainerReceive
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Detect which Signal API mode is available by probing endpoints.
|
|
868
|
+
* Native wins when both APIs are healthy because it preserves the richer JSON-RPC contract.
|
|
869
|
+
*/
|
|
870
|
+
async function detectSignalApiMode(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
|
|
871
|
+
const containerAccount = options.requireContainerReceive ? options.account?.trim() : void 0;
|
|
872
|
+
const nativeResultPromise = signalCheck$1(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
|
873
|
+
const containerResultPromise = containerAccount ? containerCheck(baseUrl, timeoutMs, containerAccount).catch(() => ({ ok: false })) : options.requireContainerReceive ? Promise.resolve({ ok: false }) : containerCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
|
874
|
+
const nativeHealthyPromise = nativeResultPromise.then((result) => {
|
|
875
|
+
if (result.ok) return "native";
|
|
876
|
+
throw new Error("native not ok");
|
|
877
|
+
});
|
|
878
|
+
const containerHealthyPromise = containerResultPromise.then((result) => {
|
|
879
|
+
if (result.ok) return "container";
|
|
880
|
+
throw new Error("container not ok");
|
|
881
|
+
});
|
|
882
|
+
try {
|
|
883
|
+
if (await Promise.any([nativeHealthyPromise, containerHealthyPromise]) === "native") return "native";
|
|
884
|
+
return (await waitForNativePreferenceGrace(nativeResultPromise)).ok ? "native" : "container";
|
|
885
|
+
} catch {
|
|
886
|
+
throw new Error(`Signal API not reachable at ${baseUrl}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Drop-in replacement for native signalRpcRequest.
|
|
891
|
+
* Routes to native JSON-RPC or container REST based on config.
|
|
892
|
+
*/
|
|
893
|
+
async function signalRpcRequest(method, params, opts) {
|
|
894
|
+
if (await resolveApiModeForOperation({
|
|
895
|
+
baseUrl: opts.baseUrl,
|
|
896
|
+
accountId: opts.accountId,
|
|
897
|
+
account: typeof params?.account === "string" ? params.account : void 0,
|
|
898
|
+
timeoutMs: opts.timeoutMs,
|
|
899
|
+
apiMode: opts.apiMode
|
|
900
|
+
}) === "native") return signalRpcRequest$1(method, params, opts);
|
|
901
|
+
return containerRpcRequest(method, params, opts);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Drop-in replacement for native signalCheck.
|
|
905
|
+
*/
|
|
906
|
+
async function signalCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
|
|
907
|
+
const configured = resolveConfiguredApiMode(options.apiMode);
|
|
908
|
+
const mode = configured === "auto" ? await resolveAutoApiMode(baseUrl, timeoutMs).catch((error) => {
|
|
909
|
+
return {
|
|
910
|
+
ok: false,
|
|
911
|
+
status: null,
|
|
912
|
+
error: formatErrorMessage$1(error)
|
|
913
|
+
};
|
|
914
|
+
}) : configured;
|
|
915
|
+
if (typeof mode !== "string") return mode;
|
|
916
|
+
if (mode === "container") return containerCheck(baseUrl, timeoutMs);
|
|
917
|
+
return signalCheck$1(baseUrl, timeoutMs);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Drop-in replacement for native streamSignalEvents.
|
|
921
|
+
* Container mode uses WebSocket; native uses SSE.
|
|
922
|
+
*/
|
|
923
|
+
async function streamSignalEvents(params) {
|
|
924
|
+
if (await resolveApiModeForOperation({
|
|
925
|
+
baseUrl: params.baseUrl,
|
|
926
|
+
accountId: params.accountId,
|
|
927
|
+
account: params.account,
|
|
928
|
+
requireContainerReceive: true,
|
|
929
|
+
timeoutMs: resolveAutoProbeTimeoutMs(params.timeoutMs),
|
|
930
|
+
apiMode: params.apiMode
|
|
931
|
+
}) === "container") return streamContainerEvents({
|
|
932
|
+
baseUrl: params.baseUrl,
|
|
933
|
+
account: params.account,
|
|
934
|
+
abortSignal: params.abortSignal,
|
|
935
|
+
timeoutMs: params.timeoutMs,
|
|
936
|
+
onEvent: (event) => params.onEvent({
|
|
937
|
+
event: "receive",
|
|
938
|
+
data: JSON.stringify(event)
|
|
939
|
+
}),
|
|
940
|
+
logger: params.logger
|
|
941
|
+
});
|
|
942
|
+
return streamSignalEvents$1({
|
|
943
|
+
baseUrl: params.baseUrl,
|
|
944
|
+
account: params.account,
|
|
945
|
+
abortSignal: params.abortSignal,
|
|
946
|
+
timeoutMs: params.timeoutMs,
|
|
947
|
+
onEvent: (event) => params.onEvent(event)
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region extensions/signal/src/rpc-context.ts
|
|
952
|
+
function resolveSignalRpcContext(opts, accountInfo) {
|
|
953
|
+
const hasBaseUrl = Boolean(normalizeOptionalString(opts.baseUrl));
|
|
954
|
+
const hasAccount = Boolean(normalizeOptionalString(opts.account));
|
|
955
|
+
if ((!hasBaseUrl || !hasAccount) && !accountInfo) throw new Error("Signal account config is required when baseUrl or account is missing");
|
|
956
|
+
const resolvedAccount = accountInfo;
|
|
957
|
+
const baseUrl = normalizeOptionalString(opts.baseUrl) ?? resolvedAccount?.baseUrl;
|
|
958
|
+
if (!baseUrl) throw new Error("Signal base URL is required");
|
|
959
|
+
return {
|
|
960
|
+
baseUrl,
|
|
961
|
+
account: normalizeOptionalString(opts.account) ?? normalizeOptionalString(resolvedAccount?.config.account)
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region extensions/signal/src/send-reactions.ts
|
|
966
|
+
function normalizeSignalId(raw) {
|
|
967
|
+
const trimmed = raw.trim();
|
|
968
|
+
if (!trimmed) return "";
|
|
969
|
+
return trimmed.replace(/^signal:/i, "").trim();
|
|
970
|
+
}
|
|
971
|
+
function normalizeSignalUuid(raw) {
|
|
972
|
+
const trimmed = normalizeSignalId(raw);
|
|
973
|
+
if (!trimmed) return "";
|
|
974
|
+
if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("uuid:")) return trimmed.slice(5).trim();
|
|
975
|
+
return trimmed;
|
|
976
|
+
}
|
|
977
|
+
function resolveTargetAuthorParams(params) {
|
|
978
|
+
const candidates = [
|
|
979
|
+
params.targetAuthor,
|
|
980
|
+
params.targetAuthorUuid,
|
|
981
|
+
params.fallback
|
|
982
|
+
];
|
|
983
|
+
for (const candidate of candidates) {
|
|
984
|
+
const raw = candidate?.trim();
|
|
985
|
+
if (!raw) continue;
|
|
986
|
+
const normalized = normalizeSignalUuid(raw);
|
|
987
|
+
if (normalized) return { targetAuthor: normalized };
|
|
988
|
+
}
|
|
989
|
+
return {};
|
|
990
|
+
}
|
|
991
|
+
async function sendReactionSignalCore(params) {
|
|
992
|
+
const cfg = requireRuntimeConfig(params.opts.cfg, "Signal reactions");
|
|
993
|
+
const apiMode = cfg.channels?.signal?.apiMode;
|
|
994
|
+
const accountInfo = resolveSignalAccount({
|
|
995
|
+
cfg,
|
|
996
|
+
accountId: params.opts.accountId
|
|
997
|
+
});
|
|
998
|
+
const { baseUrl, account } = resolveSignalRpcContext(params.opts, accountInfo);
|
|
999
|
+
const normalizedRecipient = normalizeSignalUuid(params.recipient);
|
|
1000
|
+
const groupId = params.opts.groupId?.trim();
|
|
1001
|
+
if (!normalizedRecipient && !groupId) throw new Error(params.errors.missingRecipient);
|
|
1002
|
+
if (!Number.isFinite(params.targetTimestamp) || params.targetTimestamp <= 0) throw new Error(params.errors.invalidTargetTimestamp);
|
|
1003
|
+
const normalizedEmoji = params.emoji?.trim();
|
|
1004
|
+
if (!normalizedEmoji) throw new Error(params.errors.missingEmoji);
|
|
1005
|
+
const targetAuthorParams = resolveTargetAuthorParams({
|
|
1006
|
+
targetAuthor: params.opts.targetAuthor,
|
|
1007
|
+
targetAuthorUuid: params.opts.targetAuthorUuid,
|
|
1008
|
+
fallback: normalizedRecipient
|
|
1009
|
+
});
|
|
1010
|
+
if (groupId && !targetAuthorParams.targetAuthor) throw new Error(params.errors.missingTargetAuthor);
|
|
1011
|
+
const requestParams = {
|
|
1012
|
+
emoji: normalizedEmoji,
|
|
1013
|
+
targetTimestamp: params.targetTimestamp,
|
|
1014
|
+
...params.remove ? { remove: true } : {},
|
|
1015
|
+
...targetAuthorParams
|
|
1016
|
+
};
|
|
1017
|
+
if (normalizedRecipient) requestParams.recipients = [normalizedRecipient];
|
|
1018
|
+
if (groupId) requestParams.groupIds = [groupId];
|
|
1019
|
+
if (account) requestParams.account = account;
|
|
1020
|
+
return {
|
|
1021
|
+
ok: true,
|
|
1022
|
+
timestamp: (await signalRpcRequest("sendReaction", requestParams, {
|
|
1023
|
+
baseUrl,
|
|
1024
|
+
timeoutMs: params.opts.timeoutMs,
|
|
1025
|
+
apiMode
|
|
1026
|
+
}))?.timestamp
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Send a Signal reaction to a message
|
|
1031
|
+
* @param recipient - UUID or E.164 phone number of the message author
|
|
1032
|
+
* @param targetTimestamp - Message ID (timestamp) to react to
|
|
1033
|
+
* @param emoji - Emoji to react with
|
|
1034
|
+
* @param opts - Optional account/connection overrides
|
|
1035
|
+
*/
|
|
1036
|
+
async function sendReactionSignal(recipient, targetTimestamp, emoji, opts) {
|
|
1037
|
+
return await sendReactionSignalCore({
|
|
1038
|
+
recipient,
|
|
1039
|
+
targetTimestamp,
|
|
1040
|
+
emoji,
|
|
1041
|
+
remove: false,
|
|
1042
|
+
opts,
|
|
1043
|
+
errors: {
|
|
1044
|
+
missingRecipient: "Recipient or groupId is required for Signal reaction",
|
|
1045
|
+
invalidTargetTimestamp: "Valid targetTimestamp is required for Signal reaction",
|
|
1046
|
+
missingEmoji: "Emoji is required for Signal reaction",
|
|
1047
|
+
missingTargetAuthor: "targetAuthor is required for group reactions"
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Remove a Signal reaction from a message
|
|
1053
|
+
* @param recipient - UUID or E.164 phone number of the message author
|
|
1054
|
+
* @param targetTimestamp - Message ID (timestamp) to remove reaction from
|
|
1055
|
+
* @param emoji - Emoji to remove
|
|
1056
|
+
* @param opts - Optional account/connection overrides
|
|
1057
|
+
*/
|
|
1058
|
+
async function removeReactionSignal(recipient, targetTimestamp, emoji, opts) {
|
|
1059
|
+
return await sendReactionSignalCore({
|
|
1060
|
+
recipient,
|
|
1061
|
+
targetTimestamp,
|
|
1062
|
+
emoji,
|
|
1063
|
+
remove: true,
|
|
1064
|
+
opts,
|
|
1065
|
+
errors: {
|
|
1066
|
+
missingRecipient: "Recipient or groupId is required for Signal reaction removal",
|
|
1067
|
+
invalidTargetTimestamp: "Valid targetTimestamp is required for Signal reaction removal",
|
|
1068
|
+
missingEmoji: "Emoji is required for Signal reaction removal",
|
|
1069
|
+
missingTargetAuthor: "targetAuthor is required for group reaction removal"
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
//#endregion
|
|
1074
|
+
export { signalRpcRequest as a, signalCheck as i, sendReactionSignal as n, streamSignalEvents as o, resolveSignalRpcContext as r, removeReactionSignal as t };
|