feat(WEBJS, NOWEB): message capping API and events mention #2186

This commit is contained in:
devlikepro committed 2026-08-05 14:34:59 +07:00
1 parent f26d43b299
commit 5ddbeb995e
7 files changed
+75 -15

No files matched your search

+1 -2
View File
@@ -106,8 +106,7 @@ class SessionsController {
description:
'Fetch a fresh new-chat message capping (quota) state from WhatsApp. ' +
'The same value is also available under me.messageCapping in the session ' +
'info, and changes are pushed through the session.status event. ' +
'GOWS engine only.',
'info, and changes are pushed through the session.status event.',
})
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
fetchMessageCapping(
+1 -2
View File
@@ -175,8 +175,7 @@ export class SessionTools extends McpController {
@Tool('sessions-capping', {
title: 'Get message capping',
description:
'Fetch the account new-chat message capping (per-cycle quota). ' +
'GOWS engine only.',
'Fetch the account new-chat message capping (per-cycle quota).',
inputSchema: SessionNameInput,
annotations: {
readOnlyHint: true,
@@ -13,17 +13,18 @@ function parseCappingTimestamp(value: any): number | null {
}
/**
* Normalize gows' MessageCapping payload (xwa2_message_capping_info).
* Normalize the message capping payload (xwa2_message_capping_info) - the shape is the same
* across engines: GOWS gRPC event, NOWEB Baileys fetch/push, WEBJS injected fetch/local read.
*/
export function parseGowsMessageCapping(data: any): MessageCappingData {
export function parseMessageCapping(data: any): MessageCappingData {
const cappingStatus = (data?.capping_status ??
MessageCappingStatus.NONE) as MessageCappingStatus;
return {
cappingStatus: cappingStatus,
// total/used quota are Go ints, so numbers here; -1 total means "no cap"
// -1 total means "no cap"
totalQuota: typeof data?.total_quota === 'number' ? data.total_quota : -1,
usedQuota: typeof data?.used_quota === 'number' ? data.used_quota : 0,
// cycle timestamps are unix seconds strings, absent when zero (Go's omitzero)
// cycle timestamps are unix seconds strings (or ms numbers from WEBJS local reads)
cycleStart: parseCappingTimestamp(data?.cycle_start_timestamp),
cycleEnd: parseCappingTimestamp(data?.cycle_end_timestamp),
mvStatus: data?.mv_status ?? null,
+3 -3
View File
@@ -28,7 +28,7 @@ import {
parseJsonList,
statusToAck,
} from '@waha/core/engines/gows/helpers';
import { parseGowsMessageCapping } from '@waha/core/engines/gows/capping';
import { parseMessageCapping } from '@waha/core/abc/capping';
import { parseGowsReachoutTimelock } from '@waha/core/engines/gows/reachouttimelock';
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
import {
@@ -483,7 +483,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
this.reachoutTimelock.update(parseGowsReachoutTimelock(data));
});
events.on(WhatsMeowEvent.MESSAGE_CAPPING, (data) => {
this.messageCapping.update(parseGowsMessageCapping(data));
this.messageCapping.update(parseMessageCapping(data));
});
events.on(WhatsMeowEvent.PRESENCE, (event: gows.Presence) => {
if (isJidGroup(event.From)) {
@@ -2290,7 +2290,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
const response = await promisify(this.client.FetchMessageCapping)(
this.session,
);
const capping = parseGowsMessageCapping(parseJson(response));
const capping = parseMessageCapping(parseJson(response));
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
this.messageCapping.update(capping);
return capping;
@@ -44,6 +44,7 @@ import type {
import { ILogger } from '@adiwajshing/baileys/lib/Utils/logger';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { UnprocessableEntityException } from '@nestjs/common';
import { parseMessageCapping } from '@waha/core/abc/capping';
import {
getChannelInviteLink,
getPublicUrlFromDirectPath,
@@ -175,6 +176,7 @@ import {
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import {
MeInfo,
MessageCappingData,
ReachoutTimelockEnforcementType,
} from '@waha/structures/sessions.dto';
import { EnsureSeconds } from '@waha/utils/timehelper';
@@ -562,6 +564,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
protected listenConnectionEvents() {
this.logger.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`);
this.sock.ev.on('message-capping.update', (data) => {
this.messageCapping.update(parseMessageCapping(data));
});
this.sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr, isNewLogin } = update;
if (update.reachoutTimeLock) {
@@ -590,6 +595,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.sock?.fetchAccountReachoutTimelock?.().catch((error) => {
this.logger.warn(`Failed to fetch reachout timelock: ${error}`);
});
// Same for the new-chat message capping - there is no push on (re)connect, only on changes
this.sock
?.fetchNewChatMessageCap?.()
.then((data) => {
this.messageCapping.update(parseMessageCapping(data));
})
.catch((error) => {
this.logger.warn(`Failed to fetch message capping: ${error}`);
});
// Do we need to resubscribe?
// Ideally not, we need to explicitly call interesting
// jids every 1 minute
@@ -893,6 +907,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
pushName: me.name,
lid: jidNormalizedUser(me.lid),
reachoutTimelock: this.reachoutTimelock.value,
messageCapping: this.messageCapping.value,
};
}
@@ -983,6 +998,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return true;
}
@Activity()
public async fetchMessageCapping(): Promise<MessageCappingData> {
const data = await this.sock.fetchNewChatMessageCap();
const capping = parseMessageCapping(data);
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
this.messageCapping.update(capping);
return capping;
}
/**
* Other methods
*/
@@ -1,4 +1,5 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { parseMessageCapping } from '@waha/core/abc/capping';
import {
getChannelInviteLink,
getPublicUrlFromDirectPath,
@@ -128,6 +129,7 @@ import {
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
import {
MeInfo,
MessageCappingData,
ReachoutTimelockEnforcementType,
} from '@waha/structures/sessions.dto';
import { EnsureSeconds } from '@waha/utils/timehelper';
@@ -541,6 +543,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
lid: GetSerialized(clientInfo.lid),
pushName: clientInfo?.pushname,
reachoutTimelock: this.reachoutTimelock.value,
messageCapping: this.messageCapping.value,
};
}
@@ -579,6 +582,26 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
});
});
this.whatsapp.on(Events.MESSAGE_CAPPING_UPDATE, (record: any) => {
// Unlike the timelock, a null record means "no local data yet", not "capping lifted"
if (!record) {
return;
}
this.messageCapping.update(parseMessageCapping(record));
});
this.whatsapp.on(Events.READY, async () => {
// Ask WhatsApp for the current message capping state, the same fetch the app runs on startup
try {
const data = await this.whatsapp.fetchMessageCapping();
if (data) {
this.messageCapping.update(parseMessageCapping(data));
}
} catch (err) {
this.logger.warn(err, 'Failed to fetch message capping');
}
});
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
this.logger.debug('QR received');
// Convert to image and save
@@ -812,6 +835,20 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return await this.whatsapp.deleteProfilePicture();
}
@Activity()
public async fetchMessageCapping(): Promise<MessageCappingData> {
const data = await this.whatsapp.fetchMessageCapping();
if (!data) {
throw new NotImplementedByEngineError(
'Message capping is not available in the current WhatsApp Web version',
);
}
const capping = parseMessageCapping(data);
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
this.messageCapping.update(capping);
return capping;
}
/**
* Groups methods
*/
+4 -4
View File
@@ -7,7 +7,7 @@ __metadata:
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2026-04-28":
version: 7.0.0-rc14
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=0124c8f073719727fb8db2b08d4b14b81573d82c"
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=080deabc5f48c53cf23700ec1177a8dfabc8e517"
dependencies:
"@cacheable/node-cache": "npm:^1.4.0"
"@hapi/boom": "npm:^9.1.3"
@@ -32,7 +32,7 @@ __metadata:
optional: true
link-preview-js:
optional: true
checksum: 10/35b77766d9a8eccc8366efa5a0f702b45039df3578b9ac6bc53743305255b9c7f28ca04248c50f30ed7af3d5adcd4842fdeeb557967f21d7dc096c303de8cf55
checksum: 10/4d29a66c772b1162ac32e2cfbb33fb9a4afe448ae64948948e38cb0da0d67b1e40b3be9efc51978aefad1186829b10fc41bcb8b91eec5735b7f99ad19c640fb2
languageName: node
linkType: hard
@@ -14148,7 +14148,7 @@ __metadata:
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26":
version: 1.34.7
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=ba9192e34bc57024018a88982efbad416a9279ed"
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=e8aea2332993c6dcacff650eea736dda75001e81"
dependencies:
archiver: "npm:7.0.1"
fluent-ffmpeg: "npm:2.1.3"
@@ -14165,7 +14165,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: 10/915bc0aaddc88e940f670ed026e1f9d419945c42107b6cce3ad4d3a78afe77c535c49927b1619ffe921cd906e3fe55a70d68d76bd62c9d3fe4d90b512548c83d
checksum: 10/8c6652657873f45ca3ff89bd2870180dc2c55f930bd98eeec86dbc915b005d1d1a3441df911d6f1f88c268b66f494e6ad903fe1d53061dd6f1ab076ac91c6224
languageName: node
linkType: hard