mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-18 08:43:55 +00:00
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:
commit
1b3b322674
@ -30,6 +30,7 @@ export interface InboundMessage {
|
|||||||
content: string;
|
content: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
|
isForwarded?: boolean;
|
||||||
wasMentioned?: boolean;
|
wasMentioned?: boolean;
|
||||||
isReplyToBot?: boolean;
|
isReplyToBot?: boolean;
|
||||||
media?: string[];
|
media?: string[];
|
||||||
@ -97,6 +98,10 @@ export class WhatsAppClient {
|
|||||||
return { wasMentioned, isReplyToBot };
|
return { wasMentioned, isReplyToBot };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isForwarded(msg: any): boolean {
|
||||||
|
return this.messageContextInfos(msg).some((info) => Boolean(info?.isForwarded));
|
||||||
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
const logger = pino({ level: 'silent' });
|
const logger = pino({ level: 'silent' });
|
||||||
const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir);
|
const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir);
|
||||||
@ -104,6 +109,10 @@ export class WhatsAppClient {
|
|||||||
|
|
||||||
console.log(`Using Baileys version: ${version.join('.')}`);
|
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
|
// Create socket following OpenClaw's pattern
|
||||||
this.sock = makeWASocket({
|
this.sock = makeWASocket({
|
||||||
auth: {
|
auth: {
|
||||||
@ -168,6 +177,10 @@ export class WhatsAppClient {
|
|||||||
if (msg.key.fromMe) continue;
|
if (msg.key.fromMe) continue;
|
||||||
if (msg.key.remoteJid === 'status@broadcast') 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);
|
const unwrapped = baileysExtractMessageContent(msg.message);
|
||||||
if (!unwrapped) continue;
|
if (!unwrapped) continue;
|
||||||
|
|
||||||
@ -192,8 +205,24 @@ export class WhatsAppClient {
|
|||||||
fallbackContent = '[Voice Message]';
|
fallbackContent = '[Voice Message]';
|
||||||
const path = await this.downloadMedia(msg, unwrapped.audioMessage.mimetype ?? undefined);
|
const path = await this.downloadMedia(msg, unwrapped.audioMessage.mimetype ?? undefined);
|
||||||
if (path) mediaPaths.push(path);
|
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 : '') || '';
|
const finalContent = content || (mediaPaths.length === 0 ? fallbackContent : '') || '';
|
||||||
if (!finalContent && mediaPaths.length === 0) continue;
|
if (!finalContent && mediaPaths.length === 0) continue;
|
||||||
|
|
||||||
@ -208,6 +237,7 @@ export class WhatsAppClient {
|
|||||||
content: finalContent,
|
content: finalContent,
|
||||||
timestamp: msg.messageTimestamp as number,
|
timestamp: msg.messageTimestamp as number,
|
||||||
isGroup,
|
isGroup,
|
||||||
|
...(isForwarded ? { isForwarded } : {}),
|
||||||
...(isGroup ? { wasMentioned: wasMentioned || isReplyToBot, isReplyToBot } : {}),
|
...(isGroup ? { wasMentioned: wasMentioned || isReplyToBot, isReplyToBot } : {}),
|
||||||
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
|
...(mediaPaths.length > 0 ? { media: mediaPaths } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -290,6 +290,7 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
"message_id": message_id,
|
"message_id": message_id,
|
||||||
"timestamp": data.get("timestamp"),
|
"timestamp": data.get("timestamp"),
|
||||||
"is_group": data.get("isGroup", False),
|
"is_group": data.get("isGroup", False),
|
||||||
|
"is_forwarded": bool(data.get("isForwarded", False)),
|
||||||
"participant": participant or None,
|
"participant": participant or None,
|
||||||
"is_reply_to_bot": data.get("isReplyToBot", False),
|
"is_reply_to_bot": data.get("isReplyToBot", False),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -291,6 +291,30 @@ async def test_voice_message_transcription_uses_media_path():
|
|||||||
assert kwargs["content"].startswith("Hello world")
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_unauthorized_voice_message_does_not_transcribe() -> None:
|
async def test_unauthorized_voice_message_does_not_transcribe() -> None:
|
||||||
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["allowed"]}, MagicMock())
|
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["allowed"]}, MagicMock())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user