chore: sync source updates
parent
ea745764d7
commit
800c7a1e1f
|
|
@ -2,12 +2,7 @@ import crypto from "node:crypto";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
|
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
|
||||||
import { CLAUDE_BIN, CLAUDE_IDENTITY_PREFIX, parseClaudeJson } from "./claude.js";
|
import { loadConfig, type WarelayConfig } from "../config/config.js";
|
||||||
import {
|
|
||||||
applyTemplate,
|
|
||||||
type MsgContext,
|
|
||||||
type TemplateContext,
|
|
||||||
} from "./templating.js";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_IDLE_MINUTES,
|
DEFAULT_IDLE_MINUTES,
|
||||||
DEFAULT_RESET_TRIGGER,
|
DEFAULT_RESET_TRIGGER,
|
||||||
|
|
@ -16,16 +11,25 @@ import {
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
saveSessionStore,
|
saveSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { loadConfig, type WarelayConfig } from "../config/config.js";
|
|
||||||
import { info, isVerbose, logVerbose } from "../globals.js";
|
import { info, isVerbose, logVerbose } from "../globals.js";
|
||||||
import { enqueueCommand } from "../process/command-queue.js";
|
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
|
||||||
import { sendTypingIndicator } from "../twilio/typing.js";
|
|
||||||
import type { TwilioRequester } from "../twilio/types.js";
|
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
|
||||||
import { logError } from "../logger.js";
|
import { logError } from "../logger.js";
|
||||||
import { ensureMediaHosted } from "../media/host.js";
|
import { ensureMediaHosted } from "../media/host.js";
|
||||||
import { normalizeMediaSource, splitMediaFromOutput } from "../media/parse.js";
|
import { splitMediaFromOutput } from "../media/parse.js";
|
||||||
|
import { enqueueCommand } from "../process/command-queue.js";
|
||||||
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { TwilioRequester } from "../twilio/types.js";
|
||||||
|
import { sendTypingIndicator } from "../twilio/typing.js";
|
||||||
|
import {
|
||||||
|
CLAUDE_BIN,
|
||||||
|
CLAUDE_IDENTITY_PREFIX,
|
||||||
|
parseClaudeJson,
|
||||||
|
} from "./claude.js";
|
||||||
|
import {
|
||||||
|
applyTemplate,
|
||||||
|
type MsgContext,
|
||||||
|
type TemplateContext,
|
||||||
|
} from "./templating.js";
|
||||||
|
|
||||||
type GetReplyOptions = {
|
type GetReplyOptions = {
|
||||||
onReplyStart?: () => Promise<void> | void;
|
onReplyStart?: () => Promise<void> | void;
|
||||||
|
|
@ -168,8 +172,7 @@ export async function getReplyFromConfig(
|
||||||
const prefixedBody = bodyPrefix
|
const prefixedBody = bodyPrefix
|
||||||
? `${bodyPrefix}${sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""}`
|
? `${bodyPrefix}${sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""}`
|
||||||
: (sessionCtx.BodyStripped ?? sessionCtx.Body);
|
: (sessionCtx.BodyStripped ?? sessionCtx.Body);
|
||||||
const mediaNote =
|
const mediaNote = ctx.MediaPath?.length
|
||||||
ctx.MediaPath && ctx.MediaPath.length
|
|
||||||
? `[media attached: ${ctx.MediaPath}${ctx.MediaType ? ` (${ctx.MediaType})` : ""}${ctx.MediaUrl ? ` | ${ctx.MediaUrl}` : ""}]`
|
? `[media attached: ${ctx.MediaPath}${ctx.MediaType ? ` (${ctx.MediaType})` : ""}${ctx.MediaUrl ? ` | ${ctx.MediaUrl}` : ""}]`
|
||||||
: undefined;
|
: undefined;
|
||||||
// For command prompts we prepend the media note so Claude et al. see it; text replies stay clean.
|
// For command prompts we prepend the media note so Claude et al. see it; text replies stay clean.
|
||||||
|
|
@ -208,7 +211,10 @@ const mediaNote =
|
||||||
if (reply.mode === "text" && reply.text) {
|
if (reply.mode === "text" && reply.text) {
|
||||||
await onReplyStart();
|
await onReplyStart();
|
||||||
logVerbose("Using text auto-reply from config");
|
logVerbose("Using text auto-reply from config");
|
||||||
return { text: applyTemplate(reply.text, templatingCtx), mediaUrl: reply.mediaUrl };
|
return {
|
||||||
|
text: applyTemplate(reply.text, templatingCtx),
|
||||||
|
mediaUrl: reply.mediaUrl,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reply.mode === "command" && reply.command?.length) {
|
if (reply.mode === "command" && reply.command?.length) {
|
||||||
|
|
@ -303,7 +309,10 @@ const mediaNote =
|
||||||
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
||||||
}
|
}
|
||||||
let parsed: ClaudeJsonParseResult | undefined;
|
let parsed: ClaudeJsonParseResult | undefined;
|
||||||
if (trimmed && (reply.claudeOutputFormat === "json" || isClaudeInvocation)) {
|
if (
|
||||||
|
trimmed &&
|
||||||
|
(reply.claudeOutputFormat === "json" || isClaudeInvocation)
|
||||||
|
) {
|
||||||
// Claude JSON mode: extract the human text for both logging and reply while keeping metadata.
|
// Claude JSON mode: extract the human text for both logging and reply while keeping metadata.
|
||||||
parsed = parseClaudeJson(trimmed);
|
parsed = parseClaudeJson(trimmed);
|
||||||
if (parsed?.parsed && isVerbose()) {
|
if (parsed?.parsed && isVerbose()) {
|
||||||
|
|
@ -333,7 +342,9 @@ const mediaNote =
|
||||||
logVerbose("No MEDIA token extracted from final text");
|
logVerbose("No MEDIA token extracted from final text");
|
||||||
}
|
}
|
||||||
if (!trimmed && !mediaFromCommand) {
|
if (!trimmed && !mediaFromCommand) {
|
||||||
const meta = parsed ? summarizeClaudeMetadata(parsed.parsed) : undefined;
|
const meta = parsed
|
||||||
|
? summarizeClaudeMetadata(parsed.parsed)
|
||||||
|
: undefined;
|
||||||
trimmed = `(command produced no output${meta ? `; ${meta}` : ""})`;
|
trimmed = `(command produced no output${meta ? `; ${meta}` : ""})`;
|
||||||
logVerbose("No text/media produced; injecting fallback notice to user");
|
logVerbose("No text/media produced; injecting fallback notice to user");
|
||||||
}
|
}
|
||||||
|
|
@ -373,7 +384,9 @@ const mediaNote =
|
||||||
`Command auto-reply timed out after ${elapsed}ms (limit ${timeoutMs}ms)`,
|
`Command auto-reply timed out after ${elapsed}ms (limit ${timeoutMs}ms)`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logError(`Command auto-reply failed after ${elapsed}ms: ${String(err)}`);
|
logError(
|
||||||
|
`Command auto-reply failed after ${elapsed}ms: ${String(err)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -431,7 +444,9 @@ export async function autoReplyIfConfigured(
|
||||||
`Auto-replying via Twilio: from ${replyFrom} to ${replyTo}, body length ${replyResult.text.length}`,
|
`Auto-replying via Twilio: from ${replyFrom} to ${replyTo}, body length ${replyResult.text.length}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logVerbose(`Auto-replying via Twilio: from ${replyFrom} to ${replyTo} (media)`);
|
logVerbose(
|
||||||
|
`Auto-replying via Twilio: from ${replyFrom} to ${replyTo} (media)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
|
import { autoReplyIfConfigured } from "../auto-reply/reply.js";
|
||||||
|
import { readEnv } from "../env.js";
|
||||||
|
import { info } from "../globals.js";
|
||||||
import { ensureBinary } from "../infra/binaries.js";
|
import { ensureBinary } from "../infra/binaries.js";
|
||||||
import { ensurePortAvailable, handlePortError } from "../infra/ports.js";
|
import { ensurePortAvailable, handlePortError } from "../infra/ports.js";
|
||||||
import { ensureFunnel, getTailnetHostname } from "../infra/tailscale.js";
|
import { ensureFunnel, getTailnetHostname } from "../infra/tailscale.js";
|
||||||
import { waitForever } from "./wait.js";
|
import { ensureMediaHosted } from "../media/host.js";
|
||||||
import { readEnv } from "../env.js";
|
import {
|
||||||
import { monitorTwilio as monitorTwilioImpl } from "../twilio/monitor.js";
|
logWebSelfId,
|
||||||
import { sendMessage, waitForFinalStatus } from "../twilio/send.js";
|
monitorWebProvider,
|
||||||
import { sendMessageWeb, monitorWebProvider, logWebSelfId } from "../providers/web/index.js";
|
sendMessageWeb,
|
||||||
import { assertProvider, sleep } from "../utils.js";
|
} from "../providers/web/index.js";
|
||||||
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { createClient } from "../twilio/client.js";
|
import { createClient } from "../twilio/client.js";
|
||||||
import { listRecentMessages } from "../twilio/messages.js";
|
import { listRecentMessages } from "../twilio/messages.js";
|
||||||
import { updateWebhook } from "../webhook/update.js";
|
import { monitorTwilio as monitorTwilioImpl } from "../twilio/monitor.js";
|
||||||
|
import { sendMessage, waitForFinalStatus } from "../twilio/send.js";
|
||||||
import { findWhatsappSenderSid } from "../twilio/senders.js";
|
import { findWhatsappSenderSid } from "../twilio/senders.js";
|
||||||
|
import { assertProvider, sleep } from "../utils.js";
|
||||||
import { startWebhook } from "../webhook/server.js";
|
import { startWebhook } from "../webhook/server.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { updateWebhook } from "../webhook/update.js";
|
||||||
import { info } from "../globals.js";
|
import { waitForever } from "./wait.js";
|
||||||
import { autoReplyIfConfigured } from "../auto-reply/reply.js";
|
|
||||||
import { ensureMediaHosted } from "../media/host.js";
|
|
||||||
|
|
||||||
export type CliDeps = {
|
export type CliDeps = {
|
||||||
sendMessage: typeof sendMessage;
|
sendMessage: typeof sendMessage;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
|
||||||
import { setVerbose, setYes, danger, info, warn } from "../globals.js";
|
|
||||||
import { defaultRuntime } from "../runtime.js";
|
|
||||||
import { sendCommand } from "../commands/send.js";
|
import { sendCommand } from "../commands/send.js";
|
||||||
import { statusCommand } from "../commands/status.js";
|
import { statusCommand } from "../commands/status.js";
|
||||||
import { upCommand } from "../commands/up.js";
|
import { upCommand } from "../commands/up.js";
|
||||||
import { webhookCommand } from "../commands/webhook.js";
|
import { webhookCommand } from "../commands/webhook.js";
|
||||||
import { loginWeb, monitorWebProvider } from "../provider-web.js";
|
|
||||||
import { pickProvider } from "../provider-web.js";
|
|
||||||
import type { Provider } from "../utils.js";
|
|
||||||
import { createDefaultDeps, logWebSelfId, logTwilioFrom, monitorTwilio } from "./deps.js";
|
|
||||||
import { ensureTwilioEnv } from "../env.js";
|
import { ensureTwilioEnv } from "../env.js";
|
||||||
|
import { danger, info, setVerbose, setYes, warn } from "../globals.js";
|
||||||
|
import { loginWeb, monitorWebProvider, pickProvider } from "../provider-web.js";
|
||||||
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import type { Provider } from "../utils.js";
|
||||||
|
import {
|
||||||
|
createDefaultDeps,
|
||||||
|
logTwilioFrom,
|
||||||
|
logWebSelfId,
|
||||||
|
monitorTwilio,
|
||||||
|
} from "./deps.js";
|
||||||
import { spawnRelayTmux } from "./relay_tmux.js";
|
import { spawnRelayTmux } from "./relay_tmux.js";
|
||||||
|
|
||||||
export function buildProgram() {
|
export function buildProgram() {
|
||||||
|
|
@ -57,9 +60,20 @@ export function buildProgram() {
|
||||||
"Recipient number in E.164 (e.g. +15551234567)",
|
"Recipient number in E.164 (e.g. +15551234567)",
|
||||||
)
|
)
|
||||||
.requiredOption("-m, --message <text>", "Message body")
|
.requiredOption("-m, --message <text>", "Message body")
|
||||||
.option("--media <path-or-url>", "Attach image (<=5MB). Web: path or URL. Twilio: https URL or local path hosted via webhook/funnel.")
|
.option(
|
||||||
.option("--serve-media", "For Twilio: start a temporary media server if webhook is not running", false)
|
"--media <path-or-url>",
|
||||||
.option("-w, --wait <seconds>", "Wait for delivery status (0 to skip)", "20")
|
"Attach image (<=5MB). Web: path or URL. Twilio: https URL or local path hosted via webhook/funnel.",
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--serve-media",
|
||||||
|
"For Twilio: start a temporary media server if webhook is not running",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"-w, --wait <seconds>",
|
||||||
|
"Wait for delivery status (0 to skip)",
|
||||||
|
"20",
|
||||||
|
)
|
||||||
.option("-p, --poll <seconds>", "Polling interval while waiting", "2")
|
.option("-p, --poll <seconds>", "Polling interval while waiting", "2")
|
||||||
.option("--provider <provider>", "Provider: twilio | web", "twilio")
|
.option("--provider <provider>", "Provider: twilio | web", "twilio")
|
||||||
.option("--dry-run", "Print payload and skip sending", false)
|
.option("--dry-run", "Print payload and skip sending", false)
|
||||||
|
|
@ -201,7 +215,9 @@ With Tailscale:
|
||||||
try {
|
try {
|
||||||
const server = await webhookCommand(opts, deps, defaultRuntime);
|
const server = await webhookCommand(opts, deps, defaultRuntime);
|
||||||
if (!server) {
|
if (!server) {
|
||||||
defaultRuntime.log(info("Webhook dry-run complete; no server started."));
|
defaultRuntime.log(
|
||||||
|
info("Webhook dry-run complete; no server started."),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
|
|
@ -226,7 +242,11 @@ With Tailscale:
|
||||||
.option("--path <path>", "Webhook path", "/webhook/whatsapp")
|
.option("--path <path>", "Webhook path", "/webhook/whatsapp")
|
||||||
.option("--verbose", "Verbose logging during setup/webhook", false)
|
.option("--verbose", "Verbose logging during setup/webhook", false)
|
||||||
.option("-y, --yes", "Auto-confirm prompts when possible", false)
|
.option("-y, --yes", "Auto-confirm prompts when possible", false)
|
||||||
.option("--dry-run", "Print planned actions without touching network", false)
|
.option(
|
||||||
|
"--dry-run",
|
||||||
|
"Print planned actions without touching network",
|
||||||
|
false,
|
||||||
|
)
|
||||||
// istanbul ignore next
|
// istanbul ignore next
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
setVerbose(Boolean(opts.verbose));
|
setVerbose(Boolean(opts.verbose));
|
||||||
|
|
@ -258,27 +278,36 @@ With Tailscale:
|
||||||
)
|
)
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
try {
|
try {
|
||||||
const session = await spawnRelayTmux("pnpm warelay relay --verbose", true);
|
const session = await spawnRelayTmux(
|
||||||
|
"pnpm warelay relay --verbose",
|
||||||
|
true,
|
||||||
|
);
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
info(
|
info(
|
||||||
`tmux session started and attached: ${session} (pane running "pnpm warelay relay --verbose")`,
|
`tmux session started and attached: ${session} (pane running "pnpm warelay relay --verbose")`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
defaultRuntime.error(danger(`Failed to start relay tmux session: ${String(err)}`));
|
defaultRuntime.error(
|
||||||
|
danger(`Failed to start relay tmux session: ${String(err)}`),
|
||||||
|
);
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("relay:tmux:attach")
|
.command("relay:tmux:attach")
|
||||||
.description("Attach to the existing warelay-relay tmux session (no restart)")
|
.description(
|
||||||
|
"Attach to the existing warelay-relay tmux session (no restart)",
|
||||||
|
)
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
try {
|
try {
|
||||||
await spawnRelayTmux("pnpm warelay relay --verbose", true, false);
|
await spawnRelayTmux("pnpm warelay relay --verbose", true, false);
|
||||||
defaultRuntime.log(info("Attached to warelay-relay session."));
|
defaultRuntime.log(info("Attached to warelay-relay session."));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
defaultRuntime.error(danger(`Failed to attach to warelay-relay: ${String(err)}`));
|
defaultRuntime.error(
|
||||||
|
danger(`Failed to attach to warelay-relay: ${String(err)}`),
|
||||||
|
);
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import readline from "node:readline/promises";
|
|
||||||
import { stdin as input, stdout as output } from "node:process";
|
import { stdin as input, stdout as output } from "node:process";
|
||||||
|
import readline from "node:readline/promises";
|
||||||
|
|
||||||
import { isVerbose, isYes } from "../globals.js";
|
import { isVerbose, isYes } from "../globals.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { info } from "../globals.js";
|
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import type { Provider } from "../utils.js";
|
import { info } from "../globals.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { Provider } from "../utils.js";
|
||||||
|
|
||||||
export async function sendCommand(
|
export async function sendCommand(
|
||||||
opts: {
|
opts: {
|
||||||
|
|
@ -40,14 +40,10 @@ export async function sendCommand(
|
||||||
runtime.log(info("Wait/poll are Twilio-only; ignored for provider=web."));
|
runtime.log(info("Wait/poll are Twilio-only; ignored for provider=web."));
|
||||||
}
|
}
|
||||||
const res = await deps
|
const res = await deps
|
||||||
.sendMessageWeb(
|
.sendMessageWeb(opts.to, opts.message, {
|
||||||
opts.to,
|
|
||||||
opts.message,
|
|
||||||
{
|
|
||||||
verbose: false,
|
verbose: false,
|
||||||
mediaUrl: opts.media,
|
mediaUrl: opts.media,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
runtime.error(`❌ Web send failed: ${String(err)}`);
|
runtime.error(`❌ Web send failed: ${String(err)}`);
|
||||||
throw err;
|
throw err;
|
||||||
|
|
@ -76,7 +72,7 @@ export async function sendCommand(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mediaUrl: string | undefined = undefined;
|
let mediaUrl: string | undefined;
|
||||||
if (opts.media) {
|
if (opts.media) {
|
||||||
mediaUrl = await deps.resolveTwilioMediaUrl(opts.media, {
|
mediaUrl = await deps.resolveTwilioMediaUrl(opts.media, {
|
||||||
serveMedia: Boolean(opts.serveMedia),
|
serveMedia: Boolean(opts.serveMedia),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
|
||||||
import { waitForever as defaultWaitForever } from "../cli/wait.js";
|
import { waitForever as defaultWaitForever } from "../cli/wait.js";
|
||||||
import { retryAsync } from "../infra/retry.js";
|
import { retryAsync } from "../infra/retry.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
export async function upCommand(
|
export async function upCommand(
|
||||||
opts: { port: string; path: string; verbose?: boolean; yes?: boolean; dryRun?: boolean },
|
opts: {
|
||||||
|
port: string;
|
||||||
|
path: string;
|
||||||
|
verbose?: boolean;
|
||||||
|
yes?: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
|
},
|
||||||
deps: CliDeps,
|
deps: CliDeps,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
waiter: typeof defaultWaitForever = defaultWaitForever,
|
waiter: typeof defaultWaitForever = defaultWaitForever,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
|
||||||
import { retryAsync } from "../infra/retry.js";
|
import { retryAsync } from "../infra/retry.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
export async function webhookCommand(
|
export async function webhookCommand(
|
||||||
opts: {
|
opts: {
|
||||||
|
|
@ -19,7 +19,9 @@ export async function webhookCommand(
|
||||||
}
|
}
|
||||||
await deps.ensurePortAvailable(port);
|
await deps.ensurePortAvailable(port);
|
||||||
if (opts.reply === "dry-run") {
|
if (opts.reply === "dry-run") {
|
||||||
runtime.log(`[dry-run] would start webhook on port ${port} path ${opts.path}`);
|
runtime.log(
|
||||||
|
`[dry-run] would start webhook on port ${port} path ${opts.path}`,
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const server = await retryAsync(
|
const server = await retryAsync(
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import JSON5 from "json5";
|
import JSON5 from "json5";
|
||||||
|
|
||||||
import { CONFIG_DIR, normalizeE164 } from "../utils.js";
|
|
||||||
import type { MsgContext } from "../auto-reply/templating.js";
|
import type { MsgContext } from "../auto-reply/templating.js";
|
||||||
|
import { CONFIG_DIR, normalizeE164 } from "../utils.js";
|
||||||
|
|
||||||
export type SessionScope = "per-sender" | "global";
|
export type SessionScope = "per-sender" | "global";
|
||||||
|
|
||||||
|
|
@ -22,7 +21,9 @@ export function resolveStorePath(store?: string) {
|
||||||
return path.resolve(store);
|
return path.resolve(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadSessionStore(storePath: string): Record<string, SessionEntry> {
|
export function loadSessionStore(
|
||||||
|
storePath: string,
|
||||||
|
): Record<string, SessionEntry> {
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(storePath, "utf-8");
|
const raw = fs.readFileSync(storePath, "utf-8");
|
||||||
const parsed = JSON5.parse(raw);
|
const parsed = JSON5.parse(raw);
|
||||||
|
|
|
||||||
92
src/index.ts
92
src/index.ts
|
|
@ -3,66 +3,56 @@ import process from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
|
|
||||||
import type { TwilioRequester } from "./twilio/types.js";
|
|
||||||
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
|
||||||
import { sendTypingIndicator } from "./twilio/typing.js";
|
|
||||||
import { autoReplyIfConfigured, getReplyFromConfig } from "./auto-reply/reply.js";
|
|
||||||
import { readEnv, ensureTwilioEnv, type EnvConfig } from "./env.js";
|
|
||||||
import { createClient } from "./twilio/client.js";
|
|
||||||
import { logTwilioSendError, formatTwilioError } from "./twilio/utils.js";
|
|
||||||
import { sendMessage, waitForFinalStatus } from "./twilio/send.js";
|
|
||||||
import { startWebhook as startWebhookImpl } from "./twilio/webhook.js";
|
|
||||||
import {
|
import {
|
||||||
updateWebhook as updateWebhookImpl,
|
autoReplyIfConfigured,
|
||||||
findIncomingNumberSid as findIncomingNumberSidImpl,
|
getReplyFromConfig,
|
||||||
findMessagingServiceSid as findMessagingServiceSidImpl,
|
} from "./auto-reply/reply.js";
|
||||||
setMessagingServiceWebhook as setMessagingServiceWebhookImpl,
|
import { applyTemplate } from "./auto-reply/templating.js";
|
||||||
} from "./twilio/update-webhook.js";
|
import { createDefaultDeps, monitorTwilio } from "./cli/deps.js";
|
||||||
import { listRecentMessages, formatMessageLine, uniqueBySid, sortByDateDesc } from "./twilio/messages.js";
|
import { promptYesNo } from "./cli/prompt.js";
|
||||||
import { CLAUDE_BIN } from "./auto-reply/claude.js";
|
import { waitForever } from "./cli/wait.js";
|
||||||
import { applyTemplate, type MsgContext, type TemplateContext } from "./auto-reply/templating.js";
|
import { loadConfig } from "./config/config.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH,
|
|
||||||
type WarelayConfig,
|
|
||||||
type SessionConfig,
|
|
||||||
type SessionScope,
|
|
||||||
type ReplyMode,
|
|
||||||
type ClaudeOutputFormat,
|
|
||||||
loadConfig,
|
|
||||||
} from "./config/config.js";
|
|
||||||
import { sendCommand } from "./commands/send.js";
|
|
||||||
import { statusCommand } from "./commands/status.js";
|
|
||||||
import { upCommand } from "./commands/up.js";
|
|
||||||
import { webhookCommand } from "./commands/webhook.js";
|
|
||||||
import type { Provider } from "./utils.js";
|
|
||||||
import {
|
|
||||||
assertProvider,
|
|
||||||
CONFIG_DIR,
|
|
||||||
jidToE164,
|
|
||||||
normalizeE164,
|
|
||||||
normalizePath,
|
|
||||||
sleep,
|
|
||||||
toWhatsappJid,
|
|
||||||
withWhatsAppPrefix,
|
|
||||||
} from "./utils.js";
|
|
||||||
import {
|
|
||||||
DEFAULT_IDLE_MINUTES,
|
|
||||||
DEFAULT_RESET_TRIGGER,
|
|
||||||
deriveSessionKey,
|
deriveSessionKey,
|
||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
saveSessionStore,
|
saveSessionStore,
|
||||||
SESSION_STORE_DEFAULT,
|
|
||||||
} from "./config/sessions.js";
|
} from "./config/sessions.js";
|
||||||
import { ensurePortAvailable, describePortOwner, PortInUseError, handlePortError } from "./infra/ports.js";
|
import { readEnv } from "./env.js";
|
||||||
import { ensureBinary } from "./infra/binaries.js";
|
import { ensureBinary } from "./infra/binaries.js";
|
||||||
import { ensureFunnel, ensureGoInstalled, ensureTailscaledInstalled, getTailnetHostname } from "./infra/tailscale.js";
|
import {
|
||||||
import { promptYesNo } from "./cli/prompt.js";
|
describePortOwner,
|
||||||
import { waitForever } from "./cli/wait.js";
|
ensurePortAvailable,
|
||||||
import { findWhatsappSenderSid } from "./twilio/senders.js";
|
handlePortError,
|
||||||
import { createDefaultDeps, logTwilioFrom, logWebSelfId, monitorTwilio } from "./cli/deps.js";
|
PortInUseError,
|
||||||
|
} from "./infra/ports.js";
|
||||||
|
import {
|
||||||
|
ensureFunnel,
|
||||||
|
ensureGoInstalled,
|
||||||
|
ensureTailscaledInstalled,
|
||||||
|
getTailnetHostname,
|
||||||
|
} from "./infra/tailscale.js";
|
||||||
|
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
||||||
import { monitorWebProvider } from "./provider-web.js";
|
import { monitorWebProvider } from "./provider-web.js";
|
||||||
|
import { createClient } from "./twilio/client.js";
|
||||||
|
import {
|
||||||
|
formatMessageLine,
|
||||||
|
listRecentMessages,
|
||||||
|
sortByDateDesc,
|
||||||
|
uniqueBySid,
|
||||||
|
} from "./twilio/messages.js";
|
||||||
|
import { sendMessage, waitForFinalStatus } from "./twilio/send.js";
|
||||||
|
import { findWhatsappSenderSid } from "./twilio/senders.js";
|
||||||
|
import { sendTypingIndicator } from "./twilio/typing.js";
|
||||||
|
import {
|
||||||
|
findIncomingNumberSid as findIncomingNumberSidImpl,
|
||||||
|
findMessagingServiceSid as findMessagingServiceSidImpl,
|
||||||
|
setMessagingServiceWebhook as setMessagingServiceWebhookImpl,
|
||||||
|
updateWebhook as updateWebhookImpl,
|
||||||
|
} from "./twilio/update-webhook.js";
|
||||||
|
import { formatTwilioError, logTwilioSendError } from "./twilio/utils.js";
|
||||||
|
import { startWebhook as startWebhookImpl } from "./twilio/webhook.js";
|
||||||
|
import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js";
|
||||||
|
|
||||||
dotenv.config({ quiet: true });
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
|
|
||||||
import { runExec } from "../process/exec.js";
|
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
|
||||||
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
|
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
|
||||||
import { logDebug } from "../logger.js";
|
import { logDebug } from "../logger.js";
|
||||||
|
import { runExec } from "../process/exec.js";
|
||||||
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
class PortInUseError extends Error {
|
class PortInUseError extends Error {
|
||||||
port: number;
|
port: number;
|
||||||
|
|
@ -21,7 +20,9 @@ function isErrno(err: unknown): err is NodeJS.ErrnoException {
|
||||||
return Boolean(err && typeof err === "object" && "code" in err);
|
return Boolean(err && typeof err === "object" && "code" in err);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function describePortOwner(port: number): Promise<string | undefined> {
|
export async function describePortOwner(
|
||||||
|
port: number,
|
||||||
|
): Promise<string | undefined> {
|
||||||
// Best-effort process info for a listening port (macOS/Linux).
|
// Best-effort process info for a listening port (macOS/Linux).
|
||||||
try {
|
try {
|
||||||
const { stdout } = await runExec("lsof", [
|
const { stdout } = await runExec("lsof", [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
|
|
||||||
import { promptYesNo } from "../cli/prompt.js";
|
import { promptYesNo } from "../cli/prompt.js";
|
||||||
|
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
|
||||||
import { runExec } from "../process/exec.js";
|
import { runExec } from "../process/exec.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { ensureBinary } from "./binaries.js";
|
import { ensureBinary } from "./binaries.js";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
import { danger, info, success, warn, logVerbose, isVerbose } from "./globals.js";
|
import {
|
||||||
|
danger,
|
||||||
|
info,
|
||||||
|
isVerbose,
|
||||||
|
logVerbose,
|
||||||
|
success,
|
||||||
|
warn,
|
||||||
|
} from "./globals.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "./runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "./runtime.js";
|
||||||
|
|
||||||
export function logInfo(message: string, runtime: RuntimeEnv = defaultRuntime) {
|
export function logInfo(message: string, runtime: RuntimeEnv = defaultRuntime) {
|
||||||
|
|
@ -16,7 +23,10 @@ export function logSuccess(
|
||||||
runtime.log(success(message));
|
runtime.log(success(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logError(message: string, runtime: RuntimeEnv = defaultRuntime) {
|
export function logError(
|
||||||
|
message: string,
|
||||||
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
|
) {
|
||||||
runtime.error(danger(message));
|
runtime.error(danger(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { once } from "node:events";
|
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
|
||||||
import { danger, warn } from "../globals.js";
|
|
||||||
import { logInfo, logWarn } from "../logger.js";
|
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
|
||||||
import { ensurePortAvailable, PortInUseError } from "../infra/ports.js";
|
import { ensurePortAvailable, PortInUseError } from "../infra/ports.js";
|
||||||
import { getTailnetHostname } from "../infra/tailscale.js";
|
import { getTailnetHostname } from "../infra/tailscale.js";
|
||||||
import { saveMediaSource } from "./store.js";
|
import { logInfo } from "../logger.js";
|
||||||
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { startMediaServer } from "./server.js";
|
import { startMediaServer } from "./server.js";
|
||||||
|
import { saveMediaSource } from "./store.js";
|
||||||
|
|
||||||
const DEFAULT_PORT = 42873;
|
const DEFAULT_PORT = 42873;
|
||||||
const TTL_MS = 2 * 60 * 1000;
|
const TTL_MS = 2 * 60 * 1000;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ function isValidMedia(candidate: string) {
|
||||||
if (!candidate) return false;
|
if (!candidate) return false;
|
||||||
if (candidate.length > 1024) return false;
|
if (candidate.length > 1024) return false;
|
||||||
if (/\s/.test(candidate)) return false;
|
if (/\s/.test(candidate)) return false;
|
||||||
return /^https?:\/\//i.test(candidate) || candidate.startsWith("/") || candidate.startsWith("./");
|
return (
|
||||||
|
/^https?:\/\//i.test(candidate) ||
|
||||||
|
candidate.startsWith("/") ||
|
||||||
|
candidate.startsWith("./")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitMediaFromOutput(raw: string): {
|
export function splitMediaFromOutput(raw: string): {
|
||||||
|
|
@ -29,8 +33,7 @@ export function splitMediaFromOutput(raw: string): {
|
||||||
const candidate = normalizeMediaSource(cleanCandidate(match[1]));
|
const candidate = normalizeMediaSource(cleanCandidate(match[1]));
|
||||||
const mediaUrl = isValidMedia(candidate) ? candidate : undefined;
|
const mediaUrl = isValidMedia(candidate) ? candidate : undefined;
|
||||||
|
|
||||||
const cleanedText =
|
const cleanedText = mediaUrl
|
||||||
mediaUrl
|
|
||||||
? trimmedRaw
|
? trimmedRaw
|
||||||
.replace(match[0], "")
|
.replace(match[0], "")
|
||||||
.replace(/[ \t]+\n/g, "\n")
|
.replace(/[ \t]+\n/g, "\n")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
import type { Server } from "node:http";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import express, { type Express } from "express";
|
import express, { type Express } from "express";
|
||||||
import type { Server } from "http";
|
|
||||||
import { danger } from "../globals.js";
|
import { danger } from "../globals.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { cleanOldMedia, getMediaDir } from "./store.js";
|
import { cleanOldMedia, getMediaDir } from "./store.js";
|
||||||
|
|
@ -11,7 +11,7 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000;
|
||||||
export function attachMediaRoutes(
|
export function attachMediaRoutes(
|
||||||
app: Express,
|
app: Express,
|
||||||
ttlMs = DEFAULT_TTL_MS,
|
ttlMs = DEFAULT_TTL_MS,
|
||||||
runtime: RuntimeEnv = defaultRuntime,
|
_runtime: RuntimeEnv = defaultRuntime,
|
||||||
) {
|
) {
|
||||||
const mediaDir = getMediaDir();
|
const mediaDir = getMediaDir();
|
||||||
|
|
||||||
|
|
@ -41,7 +41,6 @@ export function attachMediaRoutes(
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
void cleanOldMedia(ttlMs);
|
void cleanOldMedia(ttlMs);
|
||||||
}, ttlMs).unref();
|
}, ttlMs).unref();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startMediaServer(
|
export async function startMediaServer(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import os from "node:os";
|
|
||||||
import { pipeline } from "node:stream/promises";
|
|
||||||
import { createWriteStream } from "node:fs";
|
import { createWriteStream } from "node:fs";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
import { request } from "node:https";
|
import { request } from "node:https";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pipeline } from "node:stream/promises";
|
||||||
|
|
||||||
const MEDIA_DIR = path.join(os.homedir(), ".warelay", "media");
|
const MEDIA_DIR = path.join(os.homedir(), ".warelay", "media");
|
||||||
const MAX_BYTES = 5 * 1024 * 1024; // 5MB
|
const MAX_BYTES = 5 * 1024 * 1024; // 5MB
|
||||||
|
|
@ -58,7 +58,9 @@ async function downloadToFile(
|
||||||
req.destroy(new Error("Media exceeds 5MB limit"));
|
req.destroy(new Error("Media exceeds 5MB limit"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
pipeline(res, out).then(() => resolve()).catch(reject);
|
pipeline(res, out)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
req.on("error", reject);
|
req.on("error", reject);
|
||||||
req.end();
|
req.end();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
export { sendTypingIndicator } from "../../twilio/typing.js";
|
|
||||||
export { createClient } from "../../twilio/client.js";
|
export { createClient } from "../../twilio/client.js";
|
||||||
|
export {
|
||||||
|
formatMessageLine,
|
||||||
|
listRecentMessages,
|
||||||
|
} from "../../twilio/messages.js";
|
||||||
export { monitorTwilio } from "../../twilio/monitor.js";
|
export { monitorTwilio } from "../../twilio/monitor.js";
|
||||||
export { sendMessage, waitForFinalStatus } from "../../twilio/send.js";
|
export { sendMessage, waitForFinalStatus } from "../../twilio/send.js";
|
||||||
export { listRecentMessages, formatMessageLine } from "../../twilio/messages.js";
|
export { findWhatsappSenderSid } from "../../twilio/senders.js";
|
||||||
|
export { sendTypingIndicator } from "../../twilio/typing.js";
|
||||||
export {
|
export {
|
||||||
updateWebhook,
|
|
||||||
findIncomingNumberSid,
|
findIncomingNumberSid,
|
||||||
findMessagingServiceSid,
|
findMessagingServiceSid,
|
||||||
setMessagingServiceWebhook,
|
setMessagingServiceWebhook,
|
||||||
|
updateWebhook,
|
||||||
} from "../../twilio/update-webhook.js";
|
} from "../../twilio/update-webhook.js";
|
||||||
export { findWhatsappSenderSid } from "../../twilio/senders.js";
|
|
||||||
export { formatTwilioError, logTwilioSendError } from "../../twilio/utils.js";
|
export { formatTwilioError, logTwilioSendError } from "../../twilio/utils.js";
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
export {
|
export {
|
||||||
createWaSocket,
|
createWaSocket,
|
||||||
waitForWaConnection,
|
|
||||||
sendMessageWeb,
|
|
||||||
loginWeb,
|
loginWeb,
|
||||||
|
logWebSelfId,
|
||||||
monitorWebInbox,
|
monitorWebInbox,
|
||||||
monitorWebProvider,
|
monitorWebProvider,
|
||||||
webAuthExists,
|
|
||||||
logWebSelfId,
|
|
||||||
pickProvider,
|
pickProvider,
|
||||||
|
sendMessageWeb,
|
||||||
WA_WEB_AUTH_DIR,
|
WA_WEB_AUTH_DIR,
|
||||||
|
waitForWaConnection,
|
||||||
|
webAuthExists,
|
||||||
} from "../../provider-web.js";
|
} from "../../provider-web.js";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { withWhatsAppPrefix } from "../utils.js";
|
|
||||||
import { readEnv } from "../env.js";
|
import { readEnv } from "../env.js";
|
||||||
|
import { withWhatsAppPrefix } from "../utils.js";
|
||||||
import { createClient } from "./client.js";
|
import { createClient } from "./client.js";
|
||||||
|
|
||||||
export type ListedMessage = {
|
export type ListedMessage = {
|
||||||
|
|
@ -48,8 +48,16 @@ export async function listRecentMessages(
|
||||||
|
|
||||||
// Fetch inbound (to our WA number) and outbound (from our WA number), merge, sort, limit.
|
// Fetch inbound (to our WA number) and outbound (from our WA number), merge, sort, limit.
|
||||||
const fetchLimit = Math.min(Math.max(limit * 2, limit + 10), 100);
|
const fetchLimit = Math.min(Math.max(limit * 2, limit + 10), 100);
|
||||||
const inbound = await client.messages.list({ to: from, dateSentAfter: since, limit: fetchLimit });
|
const inbound = await client.messages.list({
|
||||||
const outbound = await client.messages.list({ from, dateSentAfter: since, limit: fetchLimit });
|
to: from,
|
||||||
|
dateSentAfter: since,
|
||||||
|
limit: fetchLimit,
|
||||||
|
});
|
||||||
|
const outbound = await client.messages.list({
|
||||||
|
from,
|
||||||
|
dateSentAfter: since,
|
||||||
|
limit: fetchLimit,
|
||||||
|
});
|
||||||
|
|
||||||
const inboundArr = Array.isArray(inbound) ? inbound : [];
|
const inboundArr = Array.isArray(inbound) ? inbound : [];
|
||||||
const outboundArr = Array.isArray(outbound) ? outbound : [];
|
const outboundArr = Array.isArray(outbound) ? outbound : [];
|
||||||
|
|
@ -85,6 +93,7 @@ export function formatMessageLine(m: ListedMessage): string {
|
||||||
? ` error ${m.errorCode}${m.errorMessage ? ` (${m.errorMessage})` : ""}`
|
? ` error ${m.errorCode}${m.errorMessage ? ` (${m.errorMessage})` : ""}`
|
||||||
: "";
|
: "";
|
||||||
const body = (m.body ?? "").replace(/\s+/g, " ").trim();
|
const body = (m.body ?? "").replace(/\s+/g, " ").trim();
|
||||||
const bodyPreview = body.length > 140 ? `${body.slice(0, 137)}…` : body || "<empty>";
|
const bodyPreview =
|
||||||
|
body.length > 140 ? `${body.slice(0, 137)}…` : body || "<empty>";
|
||||||
return `[${ts}] ${dir}${m.from ?? "?"} -> ${m.to ?? "?"} | ${status}${err} | ${bodyPreview} (sid ${m.sid})`;
|
return `[${ts}] ${dir}${m.from ?? "?"} -> ${m.to ?? "?"} | ${status}${err} | ${bodyPreview} (sid ${m.sid})`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
|
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
|
||||||
|
|
||||||
import { danger, warn } from "../globals.js";
|
|
||||||
import { sleep, withWhatsAppPrefix } from "../utils.js";
|
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
|
||||||
import { autoReplyIfConfigured } from "../auto-reply/reply.js";
|
import { autoReplyIfConfigured } from "../auto-reply/reply.js";
|
||||||
import { createClient } from "./client.js";
|
|
||||||
import { readEnv } from "../env.js";
|
import { readEnv } from "../env.js";
|
||||||
|
import { danger } from "../globals.js";
|
||||||
import { logDebug, logInfo, logWarn } from "../logger.js";
|
import { logDebug, logInfo, logWarn } from "../logger.js";
|
||||||
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
|
import { sleep, withWhatsAppPrefix } from "../utils.js";
|
||||||
|
import { createClient } from "./client.js";
|
||||||
|
|
||||||
type MonitorDeps = {
|
type MonitorDeps = {
|
||||||
autoReplyIfConfigured: typeof autoReplyIfConfigured;
|
autoReplyIfConfigured: typeof autoReplyIfConfigured;
|
||||||
|
|
@ -95,7 +94,9 @@ export async function monitorTwilio(
|
||||||
lastSeenSid = newestFirst.length ? newestFirst[0].sid : lastSeenSid;
|
lastSeenSid = newestFirst.length ? newestFirst[0].sid : lastSeenSid;
|
||||||
iterations += 1;
|
iterations += 1;
|
||||||
if (iterations >= maxIterations) break;
|
if (iterations >= maxIterations) break;
|
||||||
await deps.sleep(Math.max(pollSeconds, DEFAULT_POLL_INTERVAL_SECONDS) * 1000);
|
await deps.sleep(
|
||||||
|
Math.max(pollSeconds, DEFAULT_POLL_INTERVAL_SECONDS) * 1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { success } from "../globals.js";
|
import { readEnv } from "../env.js";
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo } from "../logger.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { withWhatsAppPrefix, sleep } from "../utils.js";
|
import { sleep, withWhatsAppPrefix } from "../utils.js";
|
||||||
import { readEnv } from "../env.js";
|
|
||||||
import { createClient } from "./client.js";
|
import { createClient } from "./client.js";
|
||||||
import { logTwilioSendError } from "./utils.js";
|
import { logTwilioSendError } from "./utils.js";
|
||||||
|
|
||||||
|
|
@ -29,7 +28,10 @@ export async function sendMessage(
|
||||||
mediaUrl: opts?.mediaUrl ? [opts.mediaUrl] : undefined,
|
mediaUrl: opts?.mediaUrl ? [opts.mediaUrl] : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
logInfo(`✅ Request accepted. Message SID: ${message.sid} -> ${toNumber}`, runtime);
|
logInfo(
|
||||||
|
`✅ Request accepted. Message SID: ${message.sid} -> ${toNumber}`,
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
return { client, sid: message.sid };
|
return { client, sid: message.sid };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logTwilioSendError(err, toNumber, runtime);
|
logTwilioSendError(err, toNumber, runtime);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { warn, isVerbose, logVerbose } from "../globals.js";
|
import { isVerbose, logVerbose, warn } from "../globals.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
type TwilioRequestOptions = {
|
type TwilioRequestOptions = {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { isVerbose, success, warn } from "../globals.js";
|
|
||||||
import { logError, logInfo } from "../logger.js";
|
|
||||||
import { readEnv } from "../env.js";
|
import { readEnv } from "../env.js";
|
||||||
import { normalizeE164 } from "../utils.js";
|
import { isVerbose } from "../globals.js";
|
||||||
|
import { logError, logInfo } from "../logger.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { createClient } from "./client.js";
|
import type { createClient } from "./client.js";
|
||||||
import type { TwilioSenderListClient, TwilioRequester } from "./types.js";
|
import type { TwilioRequester, TwilioSenderListClient } from "./types.js";
|
||||||
|
|
||||||
export async function findIncomingNumberSid(client: TwilioSenderListClient): Promise<string | null> {
|
export async function findIncomingNumberSid(
|
||||||
|
client: TwilioSenderListClient,
|
||||||
|
): Promise<string | null> {
|
||||||
// Look up incoming phone number SID matching the configured WhatsApp number.
|
// Look up incoming phone number SID matching the configured WhatsApp number.
|
||||||
try {
|
try {
|
||||||
const env = readEnv();
|
const env = readEnv();
|
||||||
|
|
@ -21,7 +22,9 @@ export async function findIncomingNumberSid(client: TwilioSenderListClient): Pro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findMessagingServiceSid(client: TwilioSenderListClient): Promise<string | null> {
|
export async function findMessagingServiceSid(
|
||||||
|
client: TwilioSenderListClient,
|
||||||
|
): Promise<string | null> {
|
||||||
// Attempt to locate a messaging service tied to the WA phone number (webhook fallback).
|
// Attempt to locate a messaging service tied to the WA phone number (webhook fallback).
|
||||||
type IncomingNumberWithService = { messagingServiceSid?: string };
|
type IncomingNumberWithService = { messagingServiceSid?: string };
|
||||||
try {
|
try {
|
||||||
|
|
@ -65,7 +68,6 @@ export async function setMessagingServiceWebhook(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Update sender webhook URL with layered fallbacks (channels, form, helper, phone).
|
// Update sender webhook URL with layered fallbacks (channels, form, helper, phone).
|
||||||
export async function updateWebhook(
|
export async function updateWebhook(
|
||||||
client: ReturnType<typeof createClient>,
|
client: ReturnType<typeof createClient>,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import express, { type Request, type Response } from "express";
|
import type { Server } from "node:http";
|
||||||
import bodyParser from "body-parser";
|
import bodyParser from "body-parser";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { Server } from "http";
|
import express, { type Request, type Response } from "express";
|
||||||
|
|
||||||
import { success, logVerbose, danger } from "../globals.js";
|
|
||||||
import { readEnv, type EnvConfig } from "../env.js";
|
|
||||||
import { createClient } from "./client.js";
|
|
||||||
import { normalizePath } from "../utils.js";
|
|
||||||
import { getReplyFromConfig, type ReplyPayload } from "../auto-reply/reply.js";
|
import { getReplyFromConfig, type ReplyPayload } from "../auto-reply/reply.js";
|
||||||
import { sendTypingIndicator } from "./typing.js";
|
import { type EnvConfig, readEnv } from "../env.js";
|
||||||
import { logTwilioSendError } from "./utils.js";
|
import { danger, success } from "../globals.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { ensureMediaHosted } from "../media/host.js";
|
||||||
import { attachMediaRoutes } from "../media/server.js";
|
import { attachMediaRoutes } from "../media/server.js";
|
||||||
import { saveMediaSource } from "../media/store.js";
|
import { saveMediaSource } from "../media/store.js";
|
||||||
import { ensureMediaHosted } from "../media/host.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
|
import { normalizePath } from "../utils.js";
|
||||||
|
import { createClient } from "./client.js";
|
||||||
|
import { sendTypingIndicator } from "./typing.js";
|
||||||
|
import { logTwilioSendError } from "./utils.js";
|
||||||
|
|
||||||
/** Start the inbound webhook HTTP server and wire optional auto-replies. */
|
/** Start the inbound webhook HTTP server and wire optional auto-replies. */
|
||||||
export async function startWebhook(
|
export async function startWebhook(
|
||||||
|
|
@ -47,18 +46,25 @@ export async function startWebhook(
|
||||||
let mediaType: string | undefined;
|
let mediaType: string | undefined;
|
||||||
if (numMedia > 0 && typeof req.body?.MediaUrl0 === "string") {
|
if (numMedia > 0 && typeof req.body?.MediaUrl0 === "string") {
|
||||||
mediaUrlInbound = req.body.MediaUrl0 as string;
|
mediaUrlInbound = req.body.MediaUrl0 as string;
|
||||||
mediaType = typeof req.body?.MediaContentType0 === "string"
|
mediaType =
|
||||||
|
typeof req.body?.MediaContentType0 === "string"
|
||||||
? (req.body.MediaContentType0 as string)
|
? (req.body.MediaContentType0 as string)
|
||||||
: undefined;
|
: undefined;
|
||||||
try {
|
try {
|
||||||
const creds = buildTwilioBasicAuth(env);
|
const creds = buildTwilioBasicAuth(env);
|
||||||
const saved = await saveMediaSource(mediaUrlInbound, {
|
const saved = await saveMediaSource(
|
||||||
|
mediaUrlInbound,
|
||||||
|
{
|
||||||
Authorization: `Basic ${creds}`,
|
Authorization: `Basic ${creds}`,
|
||||||
}, "inbound");
|
},
|
||||||
|
"inbound",
|
||||||
|
);
|
||||||
mediaPath = saved.path;
|
mediaPath = saved.path;
|
||||||
if (!mediaType && saved.contentType) mediaType = saved.contentType;
|
if (!mediaType && saved.contentType) mediaType = saved.contentType;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
runtime.error(danger(`Failed to download inbound media: ${String(err)}`));
|
runtime.error(
|
||||||
|
danger(`Failed to download inbound media: ${String(err)}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +73,15 @@ export async function startWebhook(
|
||||||
autoReply !== undefined ? { text: autoReply } : undefined;
|
autoReply !== undefined ? { text: autoReply } : undefined;
|
||||||
if (!replyResult) {
|
if (!replyResult) {
|
||||||
replyResult = await getReplyFromConfig(
|
replyResult = await getReplyFromConfig(
|
||||||
{ Body, From, To, MessageSid, MediaPath: mediaPath, MediaUrl: mediaUrlInbound, MediaType: mediaType },
|
{
|
||||||
|
Body,
|
||||||
|
From,
|
||||||
|
To,
|
||||||
|
MessageSid,
|
||||||
|
MediaPath: mediaPath,
|
||||||
|
MediaUrl: mediaUrlInbound,
|
||||||
|
MediaType: mediaType,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onReplyStart: () => sendTypingIndicator(client, runtime, MessageSid),
|
onReplyStart: () => sendTypingIndicator(client, runtime, MessageSid),
|
||||||
},
|
},
|
||||||
|
|
@ -89,9 +103,7 @@ export async function startWebhook(
|
||||||
});
|
});
|
||||||
if (verbose)
|
if (verbose)
|
||||||
runtime.log(
|
runtime.log(
|
||||||
success(
|
success(`↩️ Auto-replied to ${From}${mediaUrl ? " (media)" : ""}`),
|
||||||
`↩️ Auto-replied to ${From}${mediaUrl ? " (media)" : ""}`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logTwilioSendError(err, From ?? undefined, runtime);
|
logTwilioSendError(err, From ?? undefined, runtime);
|
||||||
|
|
@ -136,7 +148,11 @@ export async function startWebhook(
|
||||||
|
|
||||||
function buildTwilioBasicAuth(env: EnvConfig) {
|
function buildTwilioBasicAuth(env: EnvConfig) {
|
||||||
if ("authToken" in env.auth) {
|
if ("authToken" in env.auth) {
|
||||||
return Buffer.from(`${env.accountSid}:${env.auth.authToken}`).toString("base64");
|
return Buffer.from(`${env.accountSid}:${env.auth.authToken}`).toString(
|
||||||
|
"base64",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Buffer.from(`${env.auth.apiKey}:${env.auth.apiSecret}`).toString("base64");
|
return Buffer.from(`${env.auth.apiKey}:${env.auth.apiSecret}`).toString(
|
||||||
|
"base64",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
|
||||||
import { startWebhook } from "../twilio/webhook.js";
|
import { startWebhook } from "../twilio/webhook.js";
|
||||||
|
|
||||||
// Thin wrapper to keep webhook server co-located with other webhook helpers.
|
// Thin wrapper to keep webhook server co-located with other webhook helpers.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export {
|
export {
|
||||||
updateWebhook,
|
|
||||||
setMessagingServiceWebhook,
|
|
||||||
findIncomingNumberSid,
|
findIncomingNumberSid,
|
||||||
findMessagingServiceSid,
|
findMessagingServiceSid,
|
||||||
|
setMessagingServiceWebhook,
|
||||||
|
updateWebhook,
|
||||||
} from "../twilio/update-webhook.js";
|
} from "../twilio/update-webhook.js";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue