chore: release 1.2.1

main
Peter Steinberger 2025-11-28 08:11:07 +01:00
parent f63bdda628
commit c11abc1134
6 changed files with 50 additions and 32 deletions

View File

@ -1,6 +1,6 @@
# Changelog # Changelog
## 1.2.1 — Unreleased ## 1.2.1 — 2025-11-28
### Changes ### Changes
- **Manual heartbeat sends:** `warelay heartbeat` now accepts `--message/--body` with `--provider web|twilio` to push real outbound messages through the same plumbing; `--dry-run` previews payloads without sending. - **Manual heartbeat sends:** `warelay heartbeat` now accepts `--message/--body` with `--provider web|twilio` to push real outbound messages through the same plumbing; `--dry-run` previews payloads without sending.

View File

@ -1,6 +1,6 @@
{ {
"name": "warelay", "name": "warelay",
"version": "1.2.0", "version": "1.2.1",
"description": "WhatsApp relay CLI (send, monitor, webhook, auto-reply) using Twilio", "description": "WhatsApp relay CLI (send, monitor, webhook, auto-reply) using Twilio",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -1,6 +1,6 @@
import path from "node:path"; import path from "node:path";
import { mediaKindFromMime, type MediaKind } from "./constants.js"; import { type MediaKind, mediaKindFromMime } from "./constants.js";
// Map common mimes to preferred file extensions. // Map common mimes to preferred file extensions.
const EXT_BY_MIME: Record<string, string> = { const EXT_BY_MIME: Record<string, string> = {
@ -82,7 +82,10 @@ function sniffMime(buffer?: Buffer): string | undefined {
} }
// MP4: "ftyp" at offset 4. // MP4: "ftyp" at offset 4.
if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") { if (
buffer.length >= 12 &&
buffer.subarray(4, 8).toString("ascii") === "ftyp"
) {
return "video/mp4"; return "video/mp4";
} }

View File

@ -1,8 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import sharp from "sharp"; import sharp from "sharp";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const realOs = await vi.importActual<typeof import("node:os")>("node:os"); const realOs = await vi.importActual<typeof import("node:os")>("node:os");
const HOME = path.join(realOs.tmpdir(), "warelay-home-test"); const HOME = path.join(realOs.tmpdir(), "warelay-home-test");

View File

@ -41,15 +41,14 @@ function looksLikeUrl(src: string) {
return /^https?:\/\//i.test(src); return /^https?:\/\//i.test(src);
} }
/**
* Download media to disk while capturing the first few KB for mime sniffing.
*/
async function downloadToFile( async function downloadToFile(
url: string, url: string,
dest: string, dest: string,
headers?: Record<string, string>, headers?: Record<string, string>,
): Promise<{ headerMime?: string; sniffBuffer: Buffer; size: number }> ): Promise<{ headerMime?: string; sniffBuffer: Buffer; size: number }> {
/**
* Download media to disk while capturing the first few KB for mime sniffing.
*/
{
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const req = request(url, { headers }, (res) => { const req = request(url, { headers }, (res) => {
if (!res.statusCode || res.statusCode >= 400) { if (!res.statusCode || res.statusCode >= 400) {
@ -72,9 +71,14 @@ async function downloadToFile(
}); });
pipeline(res, out) pipeline(res, out)
.then(() => { .then(() => {
const sniffBuffer = Buffer.concat(sniffChunks, Math.min(sniffLen, 16384)); const sniffBuffer = Buffer.concat(
sniffChunks,
Math.min(sniffLen, 16384),
);
const rawHeader = res.headers["content-type"]; const rawHeader = res.headers["content-type"];
const headerMime = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader; const headerMime = Array.isArray(rawHeader)
? rawHeader[0]
: rawHeader;
resolve({ resolve({
headerMime, headerMime,
sniffBuffer, sniffBuffer,
@ -116,7 +120,8 @@ export async function saveMediaSource(
headerMime, headerMime,
filePath: source, filePath: source,
}); });
const ext = extensionForMime(mime) ?? path.extname(new URL(source).pathname); const ext =
extensionForMime(mime) ?? path.extname(new URL(source).pathname);
const finalDest = path.join(dir, ext ? `${id}${ext}` : id); const finalDest = path.join(dir, ext ? `${id}${ext}` : id);
await fs.rename(tempDest, finalDest); await fs.rename(tempDest, finalDest);
return { id, path: finalDest, size, contentType: mime }; return { id, path: finalDest, size, contentType: mime };

View File

@ -5,22 +5,30 @@ import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const HOME = path.join(os.tmpdir(), `warelay-inbound-media-${crypto.randomUUID()}`); const HOME = path.join(
os.tmpdir(),
`warelay-inbound-media-${crypto.randomUUID()}`,
);
process.env.HOME = HOME; process.env.HOME = HOME;
vi.mock("@whiskeysockets/baileys", async () => { vi.mock("@whiskeysockets/baileys", async () => {
const actual = await vi.importActual<typeof import("@whiskeysockets/baileys")>( const actual = await vi.importActual<
"@whiskeysockets/baileys", typeof import("@whiskeysockets/baileys")
); >("@whiskeysockets/baileys");
const jpegBuffer = Buffer.from([ const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02,
0x03, 0x03, 0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x06, 0x04,
0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d, 0x0e, 0x0f, 0x10, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a,
0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10, 0x10, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x0a, 0x09, 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e,
0x00, 0x01, 0x00, 0x01, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x14, 0x00, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d, 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10, 0x10, 0xff,
0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x01, 0x00, 0x01, 0x03, 0x01, 0x11, 0x00,
0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xff, 0xd9, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x14, 0x00, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda,
0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00,
0xff, 0xd9,
]); ]);
return { return {
...actual, ...actual,
@ -63,9 +71,11 @@ describe("web inbound media saves with extension", () => {
const onMessage = vi.fn(); const onMessage = vi.fn();
const listener = await monitorWebInbox({ verbose: false, onMessage }); const listener = await monitorWebInbox({ verbose: false, onMessage });
const { createWaSocket } = await import("./session.js"); const { createWaSocket } = await import("./session.js");
const realSock = await (createWaSocket as unknown as () => Promise<{ const realSock = await (
createWaSocket as unknown as () => Promise<{
ev: import("node:events").EventEmitter; ev: import("node:events").EventEmitter;
}>)(); }>
)();
const upsert = { const upsert = {
type: "notify", type: "notify",
@ -83,9 +93,10 @@ describe("web inbound media saves with extension", () => {
expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledTimes(1);
const msg = onMessage.mock.calls[0][0]; const msg = onMessage.mock.calls[0][0];
expect(msg.mediaPath).toBeDefined(); const mediaPath = msg.mediaPath;
expect(path.extname(msg.mediaPath!)).toBe(".jpg"); expect(mediaPath).toBeDefined();
const stat = await fs.stat(msg.mediaPath!); expect(path.extname(mediaPath as string)).toBe(".jpg");
const stat = await fs.stat(mediaPath as string);
expect(stat.size).toBeGreaterThan(0); expect(stat.size).toBeGreaterThan(0);
await listener.close(); await listener.close();