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.