Skip to content
Merged
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 index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,7 @@ declare namespace WAWebJS {
ignoreQuoteErrors?: boolean;
/**
* Should the bot wait for the message send result?
* If WhatsApp reports the message as not sent, the promise is rejected
* @default false
*/
waitUntilMsgSent?: boolean;
Expand Down
51 changes: 28 additions & 23 deletions src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,32 +382,37 @@
throw 'ready timeout';
});

/**
* Current connection information
* @type {ClientInfo}
*/
this.info = new ClientInfo(
this,
await this.pupPage.evaluate(() => {
return {
...window
.require('WAWebConnModel')
.Conn.serialize(),
wid:
window
.require('WAWebUserPrefsMeUser')
.getMaybeMePnUser() ||
window
.require('WAWebUserPrefsMeUser')
.getMaybeMeLidUser(),
};
}),
);

this.interface = new InterfaceController(this);

await this.attachEventListeners();
}

/**
* Current connection information
* @type {ClientInfo}
*/
// Set on every sync, not only on first injection: if
// WWebJS was already injected (an earlier attempt failed
// after injecting, or the event fired again), READY used
// to be emitted with this.info undefined.
this.info = new ClientInfo(
this,
await this.pupPage.evaluate(() => {
return {
...window
.require('WAWebConnModel')
.Conn.serialize(),
wid:
window
.require('WAWebUserPrefsMeUser')
.getMaybeMePnUser() ||
window
.require('WAWebUserPrefsMeUser')
.getMaybeMeLidUser(),
};
}),
);

/**
* Emitted when the client has initialized and is ready to receive messages.
* @event Client#ready
Expand Down Expand Up @@ -620,25 +625,25 @@
this.pupBrowser = browser;
this.pupPage = page;

if (puppeteerOpts.defaultViewport || puppeteerOpts.fingerprintOverrides) {

Check failure on line 628 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `puppeteerOpts.defaultViewport·||·puppeteerOpts.fingerprintOverrides` with `⏎············puppeteerOpts.defaultViewport·||⏎············puppeteerOpts.fingerprintOverrides⏎········`

Check failure on line 628 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `puppeteerOpts.defaultViewport·||·puppeteerOpts.fingerprintOverrides` with `⏎············puppeteerOpts.defaultViewport·||⏎············puppeteerOpts.fingerprintOverrides⏎········`

Check failure on line 628 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `puppeteerOpts.defaultViewport·||·puppeteerOpts.fingerprintOverrides` with `⏎············puppeteerOpts.defaultViewport·||⏎············puppeteerOpts.fingerprintOverrides⏎········`

Check failure on line 628 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `puppeteerOpts.defaultViewport·||·puppeteerOpts.fingerprintOverrides` with `⏎············puppeteerOpts.defaultViewport·||⏎············puppeteerOpts.fingerprintOverrides⏎········`
const session = await page.target().createCDPSession();
if (puppeteerOpts.defaultViewport) {
const { width, height } = puppeteerOpts.defaultViewport;
await session.send('Browser.setWindowBounds', {

Check failure on line 632 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `⏎····················`

Check failure on line 632 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `⏎····················`

Check failure on line 632 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `⏎····················`

Check failure on line 632 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `⏎····················`
windowId: (await session.send('Browser.getWindowForTarget')).windowId,

Check failure on line 633 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `windowId:·(await·session.send('Browser.getWindowForTarget')` with `····windowId:·(⏎····························await·session.send('Browser.getWindowForTarget')⏎························`

Check failure on line 633 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `windowId:·(await·session.send('Browser.getWindowForTarget')` with `····windowId:·(⏎····························await·session.send('Browser.getWindowForTarget')⏎························`

Check failure on line 633 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `windowId:·(await·session.send('Browser.getWindowForTarget')` with `····windowId:·(⏎····························await·session.send('Browser.getWindowForTarget')⏎························`

Check failure on line 633 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `windowId:·(await·session.send('Browser.getWindowForTarget')` with `····windowId:·(⏎····························await·session.send('Browser.getWindowForTarget')⏎························`
bounds: { width, height },

Check failure on line 634 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `····`

Check failure on line 634 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `····`

Check failure on line 634 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `····`

Check failure on line 634 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `····`
}).catch(() => {});

Check failure on line 635 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `})` with `····})⏎····················`

Check failure on line 635 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `})` with `····})⏎····················`

Check failure on line 635 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `})` with `····})⏎····················`

Check failure on line 635 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `})` with `····})⏎····················`
}
if (puppeteerOpts.fingerprintOverrides) {
const fp = puppeteerOpts.fingerprintOverrides;
if (fp.locale) {
await session.send('Emulation.setLocaleOverride', {

Check failure on line 640 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `⏎························`

Check failure on line 640 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `⏎························`

Check failure on line 640 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `⏎························`

Check failure on line 640 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `⏎························`
locale: fp.locale,

Check failure on line 641 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `························` with `····························`

Check failure on line 641 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `························` with `····························`

Check failure on line 641 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `························` with `····························`

Check failure on line 641 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `························` with `····························`
}).catch(() => {});

Check failure on line 642 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `})` with `····})⏎························`

Check failure on line 642 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `})` with `····})⏎························`

Check failure on line 642 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `})` with `····})⏎························`

Check failure on line 642 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `})` with `····})⏎························`
}
if (fp.timezone) {
await session.send('Emulation.setTimezoneOverride', {

Check failure on line 645 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `⏎························`

Check failure on line 645 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `⏎························`

Check failure on line 645 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Insert `⏎························`

Check failure on line 645 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Insert `⏎························`
timezoneId: fp.timezone,

Check failure on line 646 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `························` with `····························`

Check failure on line 646 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `························` with `····························`

Check failure on line 646 in src/Client.js

View workflow job for this annotation

GitHub Actions / ESLint

Replace `························` with `····························`

Check failure on line 646 in src/Client.js

View workflow job for this annotation

GitHub Actions / Tests

Replace `························` with `····························`
}).catch(() => {});
}
}
Expand Down Expand Up @@ -1602,7 +1607,7 @@
* @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
* @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
* @property {boolean} [ignoreQuoteErrors = true] - Should the bot send a quoted message without the quoted message if it fails to get the quote?
* @property {boolean} [waitUntilMsgSent = false] - Should the bot wait for the message send result?
* @property {boolean} [waitUntilMsgSent = false] - Should the bot wait for the message send result? If WhatsApp reports the message as not sent, the promise is rejected
* @property {MessageMedia} [media] - Media to be sent
* @property {any} [extra] - Extra options
*/
Expand Down
147 changes: 126 additions & 21 deletions src/util/Injected/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@
exports.LoadUtils = () => {
window.WWebJS = {};

// Newer WA Web builds (2.3000.1043xxx+) no longer expose `_serialized` on a
// MsgKey; the same string is only reachable through `toString()`. Every
// page-side `Msg.get(key._serialized)` then looks messages up by `undefined`.
// Restore it as a prototype getter. The setter keeps builds that still assign
// `_serialized` in the constructor working: it stores an own property, which
// shadows the getter exactly as before. (upstream #201901)
try {
const MsgKeyProto = window.require('WAWebMsgKey').prototype;
if (!Object.getOwnPropertyDescriptor(MsgKeyProto, '_serialized')) {
Object.defineProperty(MsgKeyProto, '_serialized', {
get() {
return this.toString();
},
set(value) {
Object.defineProperty(this, '_serialized', {
value,
writable: true,
enumerable: true,
configurable: true,
});
},
configurable: true,
});
}
} catch (_) {
// WAWebMsgKey unavailable on this build — fall back to widSerialized()
}

/**
* Dual-compat serialized id helper.
* Older WhatsApp Web builds expose WID/MsgKey as `_serialized`.
Expand Down Expand Up @@ -537,6 +565,11 @@ exports.LoadUtils = () => {
// text is unaffected. Drop it before the model is built. (upstream #201923/#201922)
delete message.__x_id;

// mediaOptions.toJSON() can also carry its own (undefined) `id` that
// overwrites the MsgKey above with the same memoize error. The message
// key always wins.
message.id = newMsgKey;

// Bot's won't reply if canonicalUrl is set (linking)
if (botOptions) {
delete message.canonicalUrl;
Expand Down Expand Up @@ -630,13 +663,26 @@ exports.LoadUtils = () => {
const [msgPromise, sendMsgResultPromise] = window
.require('WAWebSendMsgChatAction')
.addAndSendMsgToChat(chat, message);
await msgPromise;
const msg = await msgPromise;

if (options.waitUntilMsgSent) await sendMsgResultPromise;
if (options.waitUntilMsgSent) {
const { SendMsgResult } = window.require(
'WAWebSendMsgResultAction',
);
const result = await sendMsgResultPromise;
if (result?.messageSendResult !== SendMsgResult.OK) {
throw new Error(
`Message was not sent: ${result?.messageSendResult}`,
);
}
}

return window
.require('WAWebCollections')
.Msg.get(newMsgKey._serialized || newMsgKey.$1);
return (
window
.require('WAWebCollections')
.Msg.get(window.WWebJS.widSerialized(newMsgKey)) ||
(typeof msg?.serialize === 'function' ? msg : undefined)
);
};

window.WWebJS.editMessage = async (msg, content, options = {}) => {
Expand Down Expand Up @@ -769,6 +815,33 @@ exports.LoadUtils = () => {
.require('WAWebPrepRawMedia')
.prepRawMedia(opaqueData, mediaParams);
const mediaData = await mediaPrep.waitForPrep();

// WA's filehash is the base64 SHA-256 of the file. Some WA Web builds
// leave it unset after prep for documents; getOrCreateMediaObject then
// fails with the opaque "Data passed to getter must include an id
// property ... but got undefined". Compute it ourselves in that case.
if (!mediaData.filehash) {
const digest = await window.crypto.subtle.digest(
'SHA-256',
await file.arrayBuffer(),
);
let binary = '';
for (const byte of new Uint8Array(digest)) {
binary += String.fromCharCode(byte);
}
const filehash = window.btoa(binary);
if (typeof mediaData.set === 'function') {
mediaData.set({ filehash });
} else {
mediaData.filehash = filehash;
}
}
if (!mediaData.filehash) {
throw new Error(
`media-fault: filehash undefined after prep (type=${mediaData.type}, mimetype=${mediaData.mimetype}, size=${file.size})`,
);
}

const mediaObject = window
.require('WAWebMediaStorage')
.getOrCreateMediaObject(mediaData.filehash);
Expand All @@ -778,10 +851,6 @@ exports.LoadUtils = () => {
isNewsletter: sendToChannel,
});

if (!mediaData.filehash) {
throw new Error('media-fault: sendToChat filehash undefined');
}

if (
(forceVoice && mediaData.type === 'ptt') ||
(sendToStatus && mediaData.type === 'audio')
Expand Down Expand Up @@ -1054,20 +1123,28 @@ exports.LoadUtils = () => {

window.WWebJS.getChats = async () => {
const chats = window.require('WAWebCollections').Chat.getModelsArray();
const chatPromises = chats.map((chat) =>
window.WWebJS.getChatModel(chat),
const results = await Promise.allSettled(
chats.map((chat) => window.WWebJS.getChatModel(chat)),
);
return await Promise.all(chatPromises);
// One broken chat (stale/LID group metadata, revoked newsletter) must
// not reject the whole list. (upstream #201910/#201934)
return results
.filter((r) => r.status === 'fulfilled' && r.value)
.map((r) => r.value);
};

window.WWebJS.getChannels = async () => {
const channels = window
.require('WAWebCollections')
.WAWebNewsletterCollection.getModelsArray();
const channelPromises = channels?.map((channel) =>
window.WWebJS.getChatModel(channel, { isChannel: true }),
const results = await Promise.allSettled(
(channels || []).map((channel) =>
window.WWebJS.getChatModel(channel, { isChannel: true }),
),
);
return await Promise.all(channelPromises);
return results
.filter((r) => r.status === 'fulfilled' && r.value)
.map((r) => r.value);
};

window.WWebJS.getChatModel = async (chat, { isChannel = false } = {}) => {
Expand All @@ -1092,7 +1169,12 @@ exports.LoadUtils = () => {
const groupMetadata =
window.require('WAWebCollections').GroupMetadata ||
window.require('WAWebCollections').WAWebGroupMetadataCollection;
await groupMetadata.update(chatWid);
try {
await groupMetadata.update(chatWid);
} catch (_) {
// LID-based group ids can fail the IndexedDB lookup
// (DataError); keep the cached metadata instead.
}
const { toPn } = window.require('WAWebLidMigrationUtils');
const serializedMetadata = chat.groupMetadata.serialize();
for (const p of serializedMetadata.participants || []) {
Expand Down Expand Up @@ -1313,15 +1395,38 @@ exports.LoadUtils = () => {
// Always call internal downloadMedia - never skip based on
// mediaStage, because cache eviction can leave stage=RESOLVED
// with empty InMemoryMediaBlobCache.
await msg.downloadMedia({
downloadEvenIfExpensive: true,
rmrReason: 1,
isUserInitiated: true,
// When the file has expired on the server, WhatsApp asks the sender's
// phone to upload it again and downloadMedia waits for that reply,
// which may never come. Stop waiting once that happens. (upstream #201932)
const { mediaData } = msg;
const canObserve =
typeof mediaData.on === 'function' &&
typeof mediaData.off === 'function';
let onStageChange;
const reuploading = new Promise((resolve) => {
if (!canObserve) return;
onStageChange = () => {
if (mediaData.mediaStage === 'REUPLOADING') resolve();
};
mediaData.on('change:mediaStage', onStageChange);
});
try {
await Promise.race([
msg.downloadMedia({
downloadEvenIfExpensive: true,
rmrReason: 1,
isUserInitiated: true,
}),
reuploading,
]);
} finally {
if (canObserve) mediaData.off('change:mediaStage', onStageChange);
}

if (
msg.mediaData.mediaStage.includes('ERROR') ||
msg.mediaData.mediaStage === 'FETCHING'
msg.mediaData.mediaStage === 'FETCHING' ||
msg.mediaData.mediaStage === 'REUPLOADING'
) {
return null;
}
Expand Down
Loading