v0.10.9
Added
-
Statuses (stories) are now readable on both engines and retained for 24 hours. Contact status
updates land in a dedicated 24-hour store as they arrive — so a status posted while the dashboard
was closed is still there — andGET /sessions/:id/statusnow serves them on the Baileys engine
too (previously whatsapp-web.js only). A newGET /sessions/:id/status/:statusId/mediastreams a
stored status image or video. -
New opt-in
status.receivedwebhook. Subscribe a webhook tostatus.receivedto be notified
when a contact posts a status; the payload carries the contact, type, caption, and media flags (no
media blob — fetch it from the media endpoint). Off by default, though a webhook subscribed to*
(all events) receives it automatically. -
The dashboard Status tab is now functional. It lists contacts with active statuses, opens a
read-only viewer for a contact's updates (image and video), and can post a text or image status,
with a recipient picker on the Baileys engine. -
Group chats now show who sent each message. In a group conversation the dashboard labels each
incoming message with the sender's name — coloured per participant and shown once per consecutive
run — so a thread is no longer an anonymous wall of text. One-to-one chats are unchanged.
Changed
- Status
recipientsis now optional on the post-status endpoints.POST /sessions/:id/status/send-text,send-image, andsend-videoaccept an omitted or empty
recipientslist. The Baileys engine still requires it — it posts to exactly that allow-list, so
an absent/empty list now 400s there with a clear message — while whatsapp-web.js, which broadcasts
to the account's status-privacy audience and always ignored the field, no longer needs a
placeholder recipient to pass validation.
Fixed
-
Contact statuses now actually ingest on the Baileys engine. The message mapper only lifted the
sender'sparticipantintoauthorfor group chats, so a status broadcast — whose chat is the
status@broadcastpseudo-JID, not a group — lost its poster, failed poster resolution, and was
silently dropped before reaching the 24h store. On a Baileys session the status list stayed empty
andstatus.receivednever fired. The poster is now mapped for status broadcasts too. -
status.receivedno longer fires twice for the same status. The webhook now dispatches only
when ingest actually inserts a new row; a duplicate delivery (an engine re-emit after a
reconnect) or a lost insert race resolves the pre-existing row without re-notifying consumers. -
The status seed skips own and already-expired statuses, and survives a bad item. The
connect-time backfill no longer ingests the account's own active statuses as if a contact had
posted them, skips statuses already past their 24-hour TTL, and one item's ingest failure no
longer aborts the rest of the backfill. -
Expired statuses disappear from the API immediately instead of at the next purge. The status
list, per-contact list, and media endpoints now filter out rows past their 24-hour expiry rather
than serving them until the 15-minute purge sweep reaps them. -
Status media is served with a sanitized Content-Type. The stored mimetype comes from
sender-controlled message metadata; anything outsideimage/*/video/*is now served as
application/octet-streamwithX-Content-Type-Options: nosniff, so the media endpoint can't be
turned into active content on the API origin. -
The status viewer plays a contact's story in the right order. Items were rendered newest-first
while the pane scrolls to the bottom on open, so the viewer opened on the oldest status and read
backwards; items are now oldest-first with the newest at the scroll position. Composing is also
blocked until the engine type has loaded, so a Baileys post can no longer go out without
recipients. -
A failed status ingest only resolves idempotently on a genuine unique-constraint violation.
Previously any save error coinciding with an already-persisted row was swallowed into returning
that row; other persistence failures (e.g. a locked database) now propagate to the caller. -
The production Docker image no longer ships the TypeScript build cache. The incremental
tsbuildinfopinned insidedist/(needed so the dev server'sdeleteOutDirwipes it with the
output) is deleted at the end of the builder stage instead of being copied into every published
image. -
npm run devno longer crashes on the second launch withCannot find module '.../dist/main'.
The TypeScript 6 upgrade moved the incremental build cache (tsbuildinfo) out ofdist/to the
project root, where the dev server'sdeleteOutDirwipe could no longer remove it. On every start
after the first, the compiler trusted the stale cache, emitted no JavaScript at all, and the
freshly spawnednode dist/maincrashed withMODULE_NOT_FOUND(#891). The cache is pinned back
insidedist/so it is wiped together with the build output, restoring a full emit on every start.
Thanks @Magnarks. -
Outbound
withSafeFetchno longer crashes the process on unread webhook response bodies.
Status-only callers (queued webhook delivery, webhook test, deprecated direct delivery) left the
undici response body unread; when the peer reset the TLS socket — or the per-request dispatcher
was destroyed — undici could emitTypeError: terminated/ECONNRESETon the body stream with
no listener, becoming a fataluncaughtExceptionthat took every WhatsApp session offline (#887).
withSafeFetchnow cancels unread bodies before tearing down the connection; callers that already
stream the body (media / plugin downloads) are unchanged. -
Expired status media now 404s instead of 500ing when the purge races a stream. A
GET /sessions/:id/status/:statusId/medialanding in the sub-second window where the 24h purge deletes
the backing file mid-request previously surfaced a generic 500; a missing file now maps to the
same 404 as an unknown or omitted status. -
A lost status-ingest race no longer leaks an orphaned media file. When two concurrent ingests
of the same status (e.g. the connect-time seed racing a live event) both wrote a media file before
the unique constraint settled the winner, the loser's file was referenced by no row and could
never be purged. The loser now deletes its own file before returning the winner's row. -
Status tab polish. The retired Phase-1 aggregate status row no longer stacks above the
per-contact list; the statuses query waits for the Status tab instead of firing on every session
select; and picking an image file then immediately switching to a URL no longer lets the
late-arriving file bytes override the URL. -
Status tab: clearer errors, a fresher viewer, and picker fixes. A failed statuses fetch now
shows an explicit error instead of looking like "no active statuses". The open status viewer
stays in sync when statuses refetch — newly arrived items appear without re-opening the contact,
and a contact whose statuses have all expired closes cleanly. The compose recipient search now
matches name, pushName, number, or JID, and selection is capped at 256 like the backend instead
of failing on submit. Contacts known only by their pushName now show it in the list and viewer
instead of a raw JID. The locales drop two dead keys, and Arabic, Hebrew, and Telugu get proper
plural forms for the status item count.