Merge PR #4226: feat(bridge): WhatsApp forwarded message detection, startup guard, and contact handling

feat(bridge): WhatsApp forwarded message detection, startup guard, and contact handling
This commit is contained in:
Xubin Ren 2026-06-13 00:05:58 +08:00 committed by GitHub
commit 1b3b322674
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 55 additions and 0 deletions

View File

@ -30,6 +30,7 @@ export interface InboundMessage {
content: string;
timestamp: number;
isGroup: boolean;
isForwarded?: boolean;
wasMentioned?: boolean;
isReplyToBot?: boolean;
media?: string[];
@ -97,6 +98,10 @@ export class WhatsAppClient {
return { wasMentioned, isReplyToBot };
}
private isForwarded(msg: any): boolean {
return this.messageContextInfos(msg).some((info) => Boolean(info?.isForwarded));
}
async connect(): Promise<void> {
const logger = pino({ level: 'silent' });
const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir);
@ -104,6 +109,10 @@ export class WhatsAppClient {
console.log(`Using Baileys version: ${version.join('.')}`);
// Record startup time — messages older than this will be ignored
// to avoid replaying history on reconnect
const startupTimestamp = Math.floor(Date.now() / 1000);
// Create socket following OpenClaw's pattern
this.sock = makeWASocket({
auth: {
@ -168,6 +177,10 @@ export class WhatsAppClient {
if (msg.key.fromMe) continue;
if (msg.key.remoteJid === 'status@broadcast') continue;
// Drop messages older than startup time (avoid replaying history on reconnect)
const msgTimestamp = msg.messageTimestamp as number;
if (msgTimestamp && msgTimestamp < startupTimestamp) continue;
const unwrapped = baileysExtractMessageContent(msg.message);
if (!unwrapped) continue;
@ -192,8 +205,24 @@ export class WhatsAppClient {
fallbackContent = '[Voice Message]';
const path = await this.downloadMedia(msg, unwrapped.audioMessage.mimetype ?? undefined);
if (path) mediaPaths.push(path);
} else if (unwrapped.contactMessage) {
// Single shared contact
const displayName = unwrapped.contactMessage.displayName || '';
const vcard = unwrapped.contactMessage.vcard || '';
fallbackContent = `[Contact: ${displayName}]\n${vcard}`;
} else if (unwrapped.contactsArrayMessage) {
// Multiple shared contacts
const vcards = unwrapped.contactsArrayMessage.contacts || [];
const parts = vcards.map((c: any) => {
const name = c.displayName || '';
const vc = c.vcard || '';
return `[Contact: ${name}]\n${vc}`;
});
fallbackContent = parts.join('\n\n');
}
const isForwarded = this.isForwarded(msg);
const finalContent = content || (mediaPaths.length === 0 ? fallbackContent : '') || '';
if (!finalContent && mediaPaths.length === 0) continue;
@ -208,6 +237,7 @@ export class WhatsAppClient {
content: finalContent,
timestamp: msg.messageTimestamp as number,
isGroup,
...(isForwarded ? { isForwarded } : {}),
...(isGroup ? { wasMentioned: wasMentioned || isReplyToBot, isReplyToBot } : {}),
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
});

View File

@ -290,6 +290,7 @@ class WhatsAppChannel(BaseChannel):
"message_id": message_id,
"timestamp": data.get("timestamp"),
"is_group": data.get("isGroup", False),
"is_forwarded": bool(data.get("isForwarded", False)),
"participant": participant or None,
"is_reply_to_bot": data.get("isReplyToBot", False),
},

View File

@ -291,6 +291,30 @@ async def test_voice_message_transcription_uses_media_path():
assert kwargs["content"].startswith("Hello world")
@pytest.mark.asyncio
async def test_forwarded_voice_message_preserves_metadata_after_transcription():
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["*"]}, MagicMock())
ch._handle_message = AsyncMock()
ch.transcribe_audio = AsyncMock(return_value="Forwarded audio text")
await ch._handle_bridge_message(
json.dumps({
"type": "message",
"id": "v-forwarded",
"sender": "12345@s.whatsapp.net",
"pn": "",
"content": "[Voice Message]",
"timestamp": 1,
"media": ["/tmp/voice.ogg"],
"isForwarded": True,
})
)
kwargs = ch._handle_message.await_args.kwargs
assert kwargs["content"] == "Forwarded audio text"
assert kwargs["metadata"]["is_forwarded"] is True
@pytest.mark.asyncio
async def test_unauthorized_voice_message_does_not_transcribe() -> None:
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["allowed"]}, MagicMock())