Skip to content

Commit 62d1b3d

Browse files
authored
[core] Implement message deduplication to prevent duplicate webhooks in GOWS (#1718)
fix #1564
1 parent 7272f0e commit 62d1b3d

File tree

2 files changed

+50
-2
lines changed

2 files changed

+50
-2
lines changed

src/core/engines/gows/session.gows.core.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ import { isFromFullSync } from '@waha/core/engines/gows/appstate';
161161
import { toVcardV3 } from '@waha/core/vcard';
162162
import { AckToStatus } from '@waha/core/utils/acks';
163163
import { ParseEventResponseType } from '@waha/core/utils/events';
164-
import { DistinctAck } from '@waha/core/utils/reactive';
164+
import { DistinctAck, DistinctMessages } from '@waha/core/utils/reactive';
165165
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
166166
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
167167
import { exclude } from '@waha/utils/reactive/ops/exclude';
@@ -477,6 +477,10 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
477477
return msg;
478478
}),
479479
mergeMap((msg) => this.processIncomingMessage(msg, true)),
480+
filter(Boolean),
481+
// Deduplicate messages by ID to prevent duplicate webhooks
482+
// @see https://github.com/devlikeapro/waha/issues/1564
483+
DistinctMessages(),
480484
share(), // share it so we don't process twice in message.any
481485
);
482486
messagesFromOthers$ = messagesFromOthers$.pipe(
@@ -485,6 +489,10 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
485489
return msg;
486490
}),
487491
mergeMap((msg) => this.processIncomingMessage(msg, true)),
492+
filter(Boolean),
493+
// Deduplicate messages by ID to prevent duplicate webhooks
494+
// @see https://github.com/devlikeapro/waha/issues/1564
495+
DistinctMessages(),
488496
share(), // share it so we don't process twice in message.any
489497
);
490498
const messagesFromAll$ = merge(messagesFromMe$, messagesFromOthers$);

src/core/utils/reactive.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
1+
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
2+
import { WAMessage } from '@waha/structures/responses.dto';
13
import { WAMessageAckBody } from '@waha/structures/webhooks.dto';
24
import { distinct, interval } from 'rxjs';
35

46
export function DistinctAck(flushEvery: number = 60_000) {
5-
// only if we havent seen this key since the last flush
7+
// only if we haven't seen this key since the last flush
68
return distinct(
79
(msg: WAMessageAckBody) => `${msg.id}-${msg.ack}-${msg.participant}`,
810
interval(flushEvery),
911
);
1012
}
13+
14+
/**
15+
* Extracts the unique WhatsApp message ID from a serialized WAHA message ID.
16+
* Message IDs have format: {fromMe}_{chatId}_{uniqueId}[_{participant}]
17+
*
18+
* The uniqueId part is what WhatsApp generates and remains constant even when
19+
* the same message is delivered with different chat identifiers (LID vs JID).
20+
*
21+
* @example
22+
* "false_13649439626@lid_ABC123" => "ABC123"
23+
* "false_2010XXXXXXX@c.us_ABC123" => "ABC123"
24+
*/
25+
function extractUniqueMessageId(messageId: string): string {
26+
const key = parseMessageIdSerialized(messageId, true);
27+
return key.id;
28+
}
29+
30+
/**
31+
* Deduplicates messages by their unique WhatsApp message ID.
32+
* This is needed to prevent duplicate webhooks for the same message,
33+
* which can happen in GOWS when receiving the first message from a new sender.
34+
*
35+
* When a new contact sends their first message, WhatsApp may deliver:
36+
* 1. Two events with the same full ID but different internal structures
37+
* 2. Two events with different chat identifiers (LID vs JID) but same unique message ID
38+
*
39+
* This function extracts the unique message ID part and deduplicates based on that,
40+
* along with fromMe flag to avoid conflicts between sent and received messages.
41+
*
42+
* @see https://github.com/devlikeapro/waha/issues/1564
43+
*/
44+
export function DistinctMessages(flushEvery: number = 60_000) {
45+
return distinct((msg: WAMessage) => {
46+
const uniqueId = extractUniqueMessageId(msg.id);
47+
// Include fromMe to distinguish sent vs received with same ID
48+
return `${msg.fromMe}_${uniqueId}`;
49+
}, interval(flushEvery));
50+
}

0 commit comments

Comments
 (0)