Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ BROWSER_INACTIVITY_TIMEOUT=120
# WhatsApp (built-in Baileys bridge — run `hermes whatsapp` to pair)
# WHATSAPP_ENABLED=false
# WHATSAPP_ALLOWED_USERS=15551234567
# WHATSAPP_SEND_READ_RECEIPTS=true # Auto-mark incoming messages as read (default: true)

# Email (IMAP/SMTP — send and receive emails as Hermes)
# For Gmail: enable 2FA → create App Password at https://myaccount.google.com/apppasswords
Expand Down
2 changes: 2 additions & 0 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ def load_gateway_config() -> GatewayConfig:
if isinstance(frc, list):
frc = ",".join(str(v) for v in frc)
os.environ["WHATSAPP_FREE_RESPONSE_CHATS"] = str(frc)
if "send_read_receipts" in whatsapp_cfg and not os.getenv("WHATSAPP_SEND_READ_RECEIPTS"):
os.environ["WHATSAPP_SEND_READ_RECEIPTS"] = str(whatsapp_cfg["send_read_receipts"]).lower()

# Matrix settings → env vars (env vars take precedence)
matrix_cfg = yaml_cfg.get("matrix", {})
Expand Down
13 changes: 13 additions & 0 deletions scripts/whatsapp-bridge/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from
import { randomBytes } from 'crypto';
import qrcode from 'qrcode-terminal';
import { matchesAllowedUser, parseAllowedUsers } from './allowlist.js';
import { shouldSendReadReceipt } from './read_receipts.js';

// Parse CLI args
const args = process.argv.slice(2);
Expand All @@ -41,6 +42,10 @@ const WHATSAPP_DEBUG =
typeof process.env.WHATSAPP_DEBUG === 'string' &&
['1', 'true', 'yes', 'on'].includes(process.env.WHATSAPP_DEBUG.toLowerCase());

const WHATSAPP_SEND_READ_RECEIPTS = !['0', 'false', 'no', 'off'].includes(
String(process.env.WHATSAPP_SEND_READ_RECEIPTS || '').toLowerCase()
);

const PORT = parseInt(getArg('port', '3000'), 10);
const SESSION_DIR = getArg('session', path.join(process.env.HOME || '~', '.hermes', 'whatsapp', 'session'));
const IMAGE_CACHE_DIR = path.join(process.env.HOME || '~', '.hermes', 'image_cache');
Expand Down Expand Up @@ -232,6 +237,14 @@ async function startSocket() {
continue;
}

if (shouldSendReadReceipt(msg, { enabled: WHATSAPP_SEND_READ_RECEIPTS })) {
sock.readMessages([msg.key]).catch((err) => {
if (WHATSAPP_DEBUG) {
console.error('[bridge] readMessages failed:', err?.message || err);
}
});
}

const messageContent = getMessageContent(msg);
const contextInfo = getContextInfo(messageContent);
const mentionedIds = Array.from(new Set((contextInfo?.mentionedJid || []).map(normalizeWhatsAppId).filter(Boolean)));
Expand Down
6 changes: 6 additions & 0 deletions scripts/whatsapp-bridge/read_receipts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function shouldSendReadReceipt(msg, { enabled }) {
if (!enabled) return false;
if (!msg?.key) return false;
if (msg.key.fromMe) return false;
return true;
}
24 changes: 24 additions & 0 deletions scripts/whatsapp-bridge/read_receipts.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';

import { shouldSendReadReceipt } from './read_receipts.js';

const inboundMsg = { key: { id: 'abc', remoteJid: '19175395595@s.whatsapp.net', fromMe: false } };
const outboundMsg = { key: { id: 'xyz', remoteJid: '19175395595@s.whatsapp.net', fromMe: true } };

test('shouldSendReadReceipt returns true for inbound messages when enabled', () => {
assert.equal(shouldSendReadReceipt(inboundMsg, { enabled: true }), true);
});

test('shouldSendReadReceipt returns false when feature is disabled', () => {
assert.equal(shouldSendReadReceipt(inboundMsg, { enabled: false }), false);
});

test('shouldSendReadReceipt returns false for fromMe messages regardless of enabled flag', () => {
assert.equal(shouldSendReadReceipt(outboundMsg, { enabled: true }), false);
});

test('shouldSendReadReceipt returns false for malformed messages without a key', () => {
assert.equal(shouldSendReadReceipt({}, { enabled: true }), false);
assert.equal(shouldSendReadReceipt(null, { enabled: true }), false);
});
1 change: 1 addition & 0 deletions website/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code, no `+`), or `*` to allow all senders |
| `WHATSAPP_ALLOW_ALL_USERS` | Allow all WhatsApp senders without an allowlist (`true`/`false`) |
| `WHATSAPP_DEBUG` | Log raw message events in the bridge for troubleshooting (`true`/`false`) |
| `WHATSAPP_SEND_READ_RECEIPTS` | Auto-mark incoming messages as read after the allowlist filter (`true`/`false`, default `true`) |
| `SIGNAL_HTTP_URL` | signal-cli daemon HTTP endpoint (for example `http://127.0.0.1:8080`) |
| `SIGNAL_ACCOUNT` | Bot phone number in E.164 format |
| `SIGNAL_ALLOWED_USERS` | Comma-separated E.164 phone numbers or UUIDs |
Expand Down
5 changes: 5 additions & 0 deletions website/docs/user-guide/messaging/whatsapp.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ WHATSAPP_MODE=bot # "bot" or "self-chat"
WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers (with country code, no +)
# WHATSAPP_ALLOWED_USERS=* # OR use * to allow everyone
# WHATSAPP_ALLOW_ALL_USERS=true # OR set this flag instead (same effect as *)

# Optional
# WHATSAPP_SEND_READ_RECEIPTS=false # Disable auto-mark-as-read for allowlisted senders (default: true)
```

:::tip Allow-all shorthand
Expand All @@ -115,10 +118,12 @@ unauthorized_dm_behavior: pair

whatsapp:
unauthorized_dm_behavior: ignore
send_read_receipts: true
```

- `unauthorized_dm_behavior: pair` is the global default. Unknown DM senders get a pairing code.
- `whatsapp.unauthorized_dm_behavior: ignore` makes WhatsApp stay silent for unauthorized DMs, which is usually the better choice for a private number.
- `whatsapp.send_read_receipts: false` disables blue-tick read receipts for allowlisted senders (defaults to `true`).

Then start the gateway:

Expand Down