Previously syncGroupsForAccount only upserted, so groups removed from
WhatsApp (deleted, bot was kicked, etc.) lingered in the DB.
Now compute the diff: any whatsapp_groups row for this account whose
wa_group_jid is not in the live fetch result is deleted. Skip the delete
sweep when the fetch returns empty — that's more likely transient than
a genuine "every group gone" signal, and we don't want to nuke valid
data on a hiccup.
Return shape gains a `removed` count alongside `synced`.
The 6.17.x line was returning 406 not-acceptable from WhatsApp's pre-key
endpoint when distributing sender keys to per-device JIDs (e.g.
40471728529510:18@s.whatsapp.net). This blocked every group send
regardless of group size.
Baileys 7.0.0-rc series tracks WhatsApp's current protocol. API is
drop-in compatible — typecheck clean, no source changes needed.
Re-pair required: 6.x signal session files are not portable to 7.x.
Group sync previously only ran once at pairing time, so groups created in
WhatsApp afterwards never showed up.
Two complementary fixes:
- 🔄 Refresh button in the groups list view triggers
syncGroupsForAccount() on demand and re-renders the menu
- session.ts now subscribes to Baileys 'groups.upsert' and 'groups.update'
events and re-syncs (debounced 1.5s) so new groups appear without
manual action
WhatsApp's pre-key endpoint returns 406 not-acceptable if ANY single JID
in the batch is in a broken state (deleted account, deactivated, etc.).
With Baileys' default behavior of asking for the whole participant list at
once, one stale member poisons the whole group send.
Chunk participant JIDs into batches of 5 and tolerate per-chunk failures.
The send fan-out then works for the participants whose sessions did land,
which covers the vast majority of real-world groups.
Also adds explicit pino logging so we can see which chunks failed during
diagnosis.
groupMetadata alone wasn't enough — Baileys won't establish individual
libsignal sessions lazily during sendMessage, so the first send to a
freshly-paired group fails per-participant. Cast to the internal
assertSessions(jids, force=true) and call it on every participant before
attempting to send.
First send to a group after pairing fails with libsignal SessionError
"No sessions" because Baileys hasn't yet established encryption sessions
with all participants. Force-fetch group metadata before sendMessage so
Baileys populates its participant map; if the first send still races,
retry once after a 1.5s delay.
Each entry in the groups list is now a button. Tapping shows a group detail
view with [📝 Send Test Text]. Operator replies with the message body and
the bot sends it to the selected WhatsApp group via the live Baileys session,
records the action in audit_log, and shows success/failure inline.
This is a small forerunner of the full reminder send pipeline that plan 2
will build out (with media, scheduling, retries). Useful right now to
validate the end-to-end Telegram-to-WhatsApp send path during pairing tests.
All flows are now reachable from /menu (alias for /start). Single message
edits in place via editMessageText for hierarchical navigation, every leaf
has ⬅ Back / ⬅ Main Menu buttons.
Menu hierarchy:
/menu → main menu
📒 Accounts → list (each account is a button)
📒 <Account> → detail (📂 Groups | 🗑 Unpair | ⬅ back)
📂 Groups → groups list (⬅ back to account, ⬅ main menu)
🗑 Unpair → confirm (✅ yes | ⬅ cancel) → done
📡 Pair New → prompt for label, operator replies as plain message
❓ Help → help text + ⬅ Main Menu
Implementation notes:
- New menus.ts module with pure render functions for each view
- New state.ts tracks pending "awaiting pair label" per Telegram user
- bot.on("message:text") consumes the pending label after Pair New
- /pair, /unpair, /groups commands still work for power users; they reuse
the same handlers behind the scenes (executePairFlow extracted from
handlePair so the menu and the command share one path)
UX improvements driven by live testing:
- setMyCommands populates Telegram's '/' picker with all commands and
descriptions, so the operator gets autocomplete instead of guessing
syntax.
- /start replies with an inline keyboard ([📒 Accounts] [📡 How to Pair]
[❓ Help]) — quick navigation without typing.
- /accounts emits one message per account with [📂 Groups] [🗑 Unpair]
inline buttons. Tapping triggers a callback (no typed labels needed).
- New callbacks module wires the buttons. Unpair shows a confirm/cancel
prompt before acting.
/pair still requires a typed label since the value is operator-defined
content rather than a selection from existing data.
When the operator misses a QR and retries /pair for the same label, the
previous pairing flow (Baileys session in memory + Telegram message id +
event listener) was still alive. Multiple listeners then raced to edit
the same QR message, surfacing as 400 'message is not modified' errors.
Fixes:
- Track one listener per account; new /pair tears down the previous one
- Stop the existing Baileys session and wipe its session dir so the new
attempt starts from a clean slate
- Skip duplicate QR pushes (Baileys can re-emit identical QR strings)
- Fall back to a fresh photo if editMessageMedia fails for any reason
Two pairing-flow fixes after live test:
- Connection Failure during pairing: Baileys announced a stale WhatsApp Web
version that the server rejected before the QR was emitted. Pull the
current version via fetchLatestBaileysVersion() at session start.
- Telegram mobile auto-converts straight quotes to curly quotes, so labels
like /pair "test 1" arrived as “test 1” and the curly quotes were never
stripped. Extend the quote-stripping regex on /pair, /unpair, /groups.
Replace corepack-prepared pnpm with `npm install -g pnpm@9.12.0` so the
binary lives in /usr/local/bin (readable by any UID) instead of root's
corepack cache. Avoids re-downloading pnpm on every container restart
when running as a non-root host user.
Also populate .env.development with real dev credentials (Postgres at
192.168.0.210/wabot, dev Telegram bot, operator Telegram ID 818380985).
Reorganize plan 1 so a long-lived `tools` container running Node 22 + pnpm
is the entry point for every install/test/typecheck/migration command. The
host only needs Docker — no Node or pnpm install required. Tasks reordered
so the tools container exists before any pnpm operation; new tasks added
for the bootstrap install and env-file population.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-state of the plan: monorepo, all DB tables migrated, dev Docker stack
running the bot service, and Telegram-driven WhatsApp pairing working
end-to-end (QR delivered, scanned, account connected, groups synced,
auto-reconnect on disconnect, restart-survival via useMultiFileAuthState).
Plans 2-4 (reminder scheduling, web dashboard, production deploy) are
referenced but not yet written.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the validated design from the brainstorming session: two-service
topology (Next.js web + Node bot) communicating via Postgres LISTEN/NOTIFY,
Baileys for WhatsApp, grammy for Telegram, pg-boss for scheduling, Drizzle
for the data model, and Docker/Gitea-registry deploy flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>