Add per-function comments and minor typing polish

main
Peter Steinberger 2025-11-24 11:40:54 +01:00
parent 1526c238bd
commit 8874ebba55
1 changed files with 18 additions and 1 deletions

View File

@ -28,6 +28,7 @@ type EnvConfig = {
}; };
function readEnv(): EnvConfig { function readEnv(): EnvConfig {
// Load and validate Twilio auth + sender configuration from env.
const accountSid = process.env.TWILIO_ACCOUNT_SID; const accountSid = process.env.TWILIO_ACCOUNT_SID;
const whatsappFrom = process.env.TWILIO_WHATSAPP_FROM; const whatsappFrom = process.env.TWILIO_WHATSAPP_FROM;
const authToken = process.env.TWILIO_AUTH_TOKEN; const authToken = process.env.TWILIO_AUTH_TOKEN;
@ -65,6 +66,7 @@ const execFileAsync = promisify(execFile);
type ExecResult = { stdout: string; stderr: string }; type ExecResult = { stdout: string; stderr: string };
async function runExec(command: string, args: string[], maxBuffer = 2_000_000): Promise<ExecResult> { async function runExec(command: string, args: string[], maxBuffer = 2_000_000): Promise<ExecResult> {
// Thin wrapper around execFile with utf8 output.
const { stdout, stderr } = await execFileAsync(command, args, { const { stdout, stderr } = await execFileAsync(command, args, {
maxBuffer, maxBuffer,
encoding: 'utf8' encoding: 'utf8'
@ -73,6 +75,7 @@ async function runExec(command: string, args: string[], maxBuffer = 2_000_000):
} }
async function ensureBinary(name: string): Promise<void> { async function ensureBinary(name: string): Promise<void> {
// Abort early if a required CLI tool is missing.
await runExec('which', [name]).catch(() => { await runExec('which', [name]).catch(() => {
console.error(`Missing required binary: ${name}. Please install it.`); console.error(`Missing required binary: ${name}. Please install it.`);
process.exit(1); process.exit(1);
@ -80,6 +83,7 @@ async function ensureBinary(name: string): Promise<void> {
} }
function withWhatsAppPrefix(number: string): string { function withWhatsAppPrefix(number: string): string {
// Ensure number has whatsapp: prefix expected by Twilio.
return number.startsWith('whatsapp:') ? number : `whatsapp:${number}`; return number.startsWith('whatsapp:') ? number : `whatsapp:${number}`;
} }
@ -99,6 +103,7 @@ type WarelayConfig = {
}; };
function loadConfig(): WarelayConfig { function loadConfig(): WarelayConfig {
// Read ~/.warelay/warelay.json (JSON5) if present.
try { try {
if (!fs.existsSync(CONFIG_PATH)) return {}; if (!fs.existsSync(CONFIG_PATH)) return {};
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8'); const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
@ -119,6 +124,7 @@ type MsgContext = {
}; };
function applyTemplate(str: string, ctx: MsgContext) { function applyTemplate(str: string, ctx: MsgContext) {
// Simple {{Placeholder}} interpolation using inbound message context.
return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => { return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
const value = (ctx as Record<string, unknown>)[key]; const value = (ctx as Record<string, unknown>)[key];
return value == null ? '' : String(value); return value == null ? '' : String(value);
@ -126,6 +132,7 @@ function applyTemplate(str: string, ctx: MsgContext) {
} }
async function getReplyFromConfig(ctx: MsgContext): Promise<string | undefined> { async function getReplyFromConfig(ctx: MsgContext): Promise<string | undefined> {
// Choose reply from config: static text or external command stdout.
const cfg = loadConfig(); const cfg = loadConfig();
const reply = cfg.inbound?.reply; const reply = cfg.inbound?.reply;
if (!reply) return undefined; if (!reply) return undefined;
@ -153,6 +160,7 @@ async function getReplyFromConfig(ctx: MsgContext): Promise<string | undefined>
} }
function createClient(env: EnvConfig) { function createClient(env: EnvConfig) {
// Twilio client using either auth token or API key/secret.
if ('authToken' in env.auth) { if ('authToken' in env.auth) {
return Twilio(env.accountSid, env.auth.authToken, { return Twilio(env.accountSid, env.auth.authToken, {
accountSid: env.accountSid accountSid: env.accountSid
@ -164,6 +172,7 @@ function createClient(env: EnvConfig) {
} }
async function sendMessage(to: string, body: string) { async function sendMessage(to: string, body: string) {
// Send outbound WhatsApp message; exit non-zero on API failure.
const env = readEnv(); const env = readEnv();
const client = createClient(env); const client = createClient(env);
const from = withWhatsAppPrefix(env.whatsappFrom); const from = withWhatsAppPrefix(env.whatsappFrom);
@ -206,6 +215,7 @@ async function waitForFinalStatus(
timeoutSeconds: number, timeoutSeconds: number,
pollSeconds: number pollSeconds: number
) { ) {
// Poll message status until delivered/failed or timeout.
const deadline = Date.now() + timeoutSeconds * 1000; const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) { while (Date.now() < deadline) {
const m = await client.messages(sid).fetch(); const m = await client.messages(sid).fetch();
@ -232,6 +242,7 @@ async function startWebhook(
path = '/webhook/whatsapp', path = '/webhook/whatsapp',
autoReply?: string autoReply?: string
) { ) {
// Start Express webhook; generate replies via config or CLI flag.
const env = readEnv(); const env = readEnv();
const app = express(); const app = express();
@ -279,6 +290,7 @@ async function startWebhook(
} }
async function getTailnetHostname() { async function getTailnetHostname() {
// Derive tailnet hostname (or IP fallback) from tailscale status JSON.
const { stdout } = await runExec('tailscale', ['status', '--json']); const { stdout } = await runExec('tailscale', ['status', '--json']);
const parsed = stdout ? (JSON.parse(stdout) as Record<string, unknown>) : {}; const parsed = stdout ? (JSON.parse(stdout) as Record<string, unknown>) : {};
const self = parsed?.['Self'] as Record<string, unknown> | undefined; const self = parsed?.['Self'] as Record<string, unknown> | undefined;
@ -290,6 +302,7 @@ async function getTailnetHostname() {
} }
async function ensureFunnel(port: number) { async function ensureFunnel(port: number) {
// Ensure Funnel is enabled and publish the webhook port.
try { try {
const statusOut = (await runExec('tailscale', ['funnel', 'status', '--json'])).stdout.trim(); const statusOut = (await runExec('tailscale', ['funnel', 'status', '--json'])).stdout.trim();
const parsed = statusOut ? (JSON.parse(statusOut) as Record<string, unknown>) : {}; const parsed = statusOut ? (JSON.parse(statusOut) as Record<string, unknown>) : {};
@ -309,6 +322,7 @@ async function ensureFunnel(port: number) {
} }
async function findWhatsappSenderSid(client: ReturnType<typeof createClient>, from: string) { async function findWhatsappSenderSid(client: ReturnType<typeof createClient>, from: string) {
// Fetch sender SID that matches configured WhatsApp from number.
const resp = await (client as unknown as { request: (options: Record<string, unknown>) => Promise<{ data?: unknown }> }).request({ const resp = await (client as unknown as { request: (options: Record<string, unknown>) => Promise<{ data?: unknown }> }).request({
method: 'get', method: 'get',
uri: 'https://messaging.twilio.com/v2/Channels/Senders', uri: 'https://messaging.twilio.com/v2/Channels/Senders',
@ -339,7 +353,8 @@ async function updateWebhook(
url: string, url: string,
method: 'POST' | 'GET' = 'POST' method: 'POST' | 'GET' = 'POST'
) { ) {
await (client as any).request({ // Point Twilio sender webhook at the provided URL.
await (client as unknown as { request: (options: Record<string, unknown>) => Promise<unknown> }).request({
method: 'post', method: 'post',
uri: `https://messaging.twilio.com/v2/Channels/Senders/${senderSid}`, uri: `https://messaging.twilio.com/v2/Channels/Senders/${senderSid}`,
form: { form: {
@ -351,10 +366,12 @@ async function updateWebhook(
} }
function sleep(ms: number) { function sleep(ms: number) {
// Promise-based sleep utility.
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
async function monitor(intervalSeconds: number, lookbackMinutes: number) { async function monitor(intervalSeconds: number, lookbackMinutes: number) {
// Poll Twilio for inbound messages and stream them with de-dupe.
const env = readEnv(); const env = readEnv();
const client = createClient(env); const client = createClient(env);
const from = withWhatsAppPrefix(env.whatsappFrom); const from = withWhatsAppPrefix(env.whatsappFrom);