whalibmob 5.10.6 → 5.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/lib/Client.js +43 -20
- package/lib/MediaThumbnail.js +68 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2149,7 +2149,7 @@ Both methods accept a **Buffer** (use `fs.readFileSync` to load a file).
|
|
|
2149
2149
|
```js
|
|
2150
2150
|
const fs = require('fs')
|
|
2151
2151
|
|
|
2152
|
-
// change your own profile picture
|
|
2152
|
+
// change your own profile picture — any image, any shape, any size
|
|
2153
2153
|
// returns the new picture id, or 'remove' when it was taken down
|
|
2154
2154
|
const picId = await client.changeProfilePicture(fs.readFileSync('./avatar.jpg'))
|
|
2155
2155
|
|
|
@@ -2165,8 +2165,31 @@ const picId = await client.changeGroupPicture('120363000000000000@g.us',
|
|
|
2165
2165
|
await client.changeGroupPicture('120363000000000000@g.us', null)
|
|
2166
2166
|
```
|
|
2167
2167
|
|
|
2168
|
-
`changeGroupPicture` throws when the server refuses — `406` for an image
|
|
2169
|
-
not
|
|
2168
|
+
`changeGroupPicture` throws when the server refuses — `406` for an image it will
|
|
2169
|
+
not take, `403` when you are not an admin of that group.
|
|
2170
|
+
|
|
2171
|
+
> [!NOTE]
|
|
2172
|
+
> The two are addressed differently, and it matters. A group picture names the
|
|
2173
|
+
> group in the IQ's `target`; your own picture names nobody — the server takes
|
|
2174
|
+
> that from the session. Your own JID in `target` is not an error the server
|
|
2175
|
+
> reports, it is a stanza it drops without answering.
|
|
2176
|
+
|
|
2177
|
+
**Both calls re-encode the image before sending it.** WhatsApp wants a square
|
|
2178
|
+
640×640 JPEG, and a picture that is not one is *dropped without an answer*
|
|
2179
|
+
rather than refused — so an unprepared file fails as a timeout that looks like a
|
|
2180
|
+
network fault. The image is cropped square, scaled and written out as a fresh
|
|
2181
|
+
JPEG, which also strips EXIF and any progressive encoding the file was carrying.
|
|
2182
|
+
|
|
2183
|
+
This needs `jimp` (an optional dependency) or `sharp`. Without either, the file
|
|
2184
|
+
is sent as it is and an already-correct picture still works.
|
|
2185
|
+
|
|
2186
|
+
```js
|
|
2187
|
+
// skip the re-encode if you have prepared the image yourself
|
|
2188
|
+
await client.changeProfilePicture(buf, { raw: true })
|
|
2189
|
+
|
|
2190
|
+
// or choose the size
|
|
2191
|
+
await client.changeProfilePicture(buf, { size: 640, quality: 50 })
|
|
2192
|
+
```
|
|
2170
2193
|
|
|
2171
2194
|
## Privacy
|
|
2172
2195
|
|
package/lib/Client.js
CHANGED
|
@@ -10,6 +10,7 @@ const { NoiseSocket } = require('./noise');
|
|
|
10
10
|
const { MessageSender, generateMessageId, makeJid, buildOrGetAdvIdentity } = require('./messages/MessageSender');
|
|
11
11
|
const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, assertRegistrationKeys, fetchIosVersion, fetchWaVersion } = require('./Registration');
|
|
12
12
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
13
|
+
const { prepareProfilePicture } = require('./MediaThumbnail');
|
|
13
14
|
const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = require('./Store');
|
|
14
15
|
const { BinaryNode } = require('./BinaryNode');
|
|
15
16
|
const { AppStateStore, COLLECTIONS } = require('./appstate/AppStateStore');
|
|
@@ -4330,30 +4331,43 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4330
4331
|
// buf: Buffer with JPEG image data
|
|
4331
4332
|
// Change your own profile picture.
|
|
4332
4333
|
//
|
|
4333
|
-
// buf: a Buffer
|
|
4334
|
+
// buf: a Buffer holding any image, or null to remove the current picture.
|
|
4334
4335
|
// Returns the new picture id, or 'remove' when it was taken down.
|
|
4335
4336
|
//
|
|
4336
|
-
// The
|
|
4337
|
-
//
|
|
4338
|
-
//
|
|
4339
|
-
//
|
|
4340
|
-
//
|
|
4341
|
-
|
|
4337
|
+
// The image is re-encoded to the square 640x640 JPEG the server expects
|
|
4338
|
+
// before it goes out. Sending a file as it came off disk is what a person
|
|
4339
|
+
// naturally tries and what the server silently drops — pass { raw: true } to
|
|
4340
|
+
// skip that if you have already prepared it yourself.
|
|
4341
|
+
//
|
|
4342
|
+
// `target` names *whose* picture is being set, and is left off entirely when
|
|
4343
|
+
// it is your own — the server infers that from the session. A group picture
|
|
4344
|
+
// carries a target and works; the same IQ with your own JID in target does
|
|
4345
|
+
// not, and the server's answer to that is silence rather than a refusal.
|
|
4346
|
+
//
|
|
4347
|
+
// This has now been wrong in both directions: first addressed *to* the
|
|
4348
|
+
// account, then addressed about it. It is neither — it is addressed to the
|
|
4349
|
+
// server with nothing named at all.
|
|
4350
|
+
async changeProfilePicture(buf, opts) {
|
|
4342
4351
|
if (!this._store) throw new Error('Not connected');
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
: `${this._store.phoneNumber}@s.whatsapp.net`;
|
|
4352
|
+
opts = opts || {};
|
|
4353
|
+
const picture = (buf && !opts.raw) ? await prepareProfilePicture(buf, opts) : buf;
|
|
4346
4354
|
|
|
4347
4355
|
const node = new BinaryNode('iq', {
|
|
4348
|
-
id:
|
|
4349
|
-
xmlns:
|
|
4350
|
-
to:
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
}, buf ? [new BinaryNode('picture', { type: 'image' }, buf)] : null);
|
|
4356
|
+
id: this._genMsgId(),
|
|
4357
|
+
xmlns: 'w:profile:picture',
|
|
4358
|
+
to: 's.whatsapp.net',
|
|
4359
|
+
type: 'set'
|
|
4360
|
+
}, picture ? [new BinaryNode('picture', { type: 'image' }, picture)] : null);
|
|
4354
4361
|
|
|
4355
4362
|
const resp = await this._sendIq(node);
|
|
4356
|
-
if (!resp)
|
|
4363
|
+
if (!resp) {
|
|
4364
|
+
throw new Error('changeProfilePicture: no reply from server (IQ timed out). ' +
|
|
4365
|
+
'The server drops a picture it will not take rather than refusing it, so ' +
|
|
4366
|
+
'this usually means the image was not in the form it wants' +
|
|
4367
|
+
(opts.raw ? ' — this call passed { raw: true }, so nothing was re-encoded'
|
|
4368
|
+
: '. Install jimp if it is not present, so the image can be ' +
|
|
4369
|
+
're-encoded before sending') + '.');
|
|
4370
|
+
}
|
|
4357
4371
|
if (resp.attrs && resp.attrs.type === 'error') {
|
|
4358
4372
|
const errNode = findChild(resp, 'error');
|
|
4359
4373
|
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
@@ -5923,7 +5937,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5923
5937
|
//
|
|
5924
5938
|
// Removal is the IQ with no body at all — an empty <picture> node is a
|
|
5925
5939
|
// malformed upload rather than a delete, and the server answers 406 for it.
|
|
5926
|
-
|
|
5940
|
+
//
|
|
5941
|
+
// The image is re-encoded to a square 640x640 JPEG first, on the same terms
|
|
5942
|
+
// as a profile picture; { raw: true } skips that.
|
|
5943
|
+
async changeGroupPicture(groupJid, buf, opts) {
|
|
5944
|
+
opts = opts || {};
|
|
5945
|
+
const picture = (buf && !opts.raw) ? await prepareProfilePicture(buf, opts) : buf;
|
|
5927
5946
|
const id = this._genMsgId();
|
|
5928
5947
|
const node = new BinaryNode('iq', {
|
|
5929
5948
|
id,
|
|
@@ -5931,9 +5950,13 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5931
5950
|
target: jidStrToObj(makeJid(groupJid)),
|
|
5932
5951
|
to: 's.whatsapp.net',
|
|
5933
5952
|
type: 'set'
|
|
5934
|
-
},
|
|
5953
|
+
}, picture ? [new BinaryNode('picture', { type: 'image' }, picture)] : null);
|
|
5935
5954
|
const resp = await this._sendIq(node);
|
|
5936
|
-
if (!resp)
|
|
5955
|
+
if (!resp) {
|
|
5956
|
+
throw new Error('changeGroupPicture: no reply from server (IQ timed out). ' +
|
|
5957
|
+
'The server drops a picture it will not take rather than refusing it, so ' +
|
|
5958
|
+
'this usually means the image was not in the form it wants.');
|
|
5959
|
+
}
|
|
5937
5960
|
if (resp.attrs && resp.attrs.type === 'error') {
|
|
5938
5961
|
const errNode = findChild(resp, 'error');
|
|
5939
5962
|
const code = errNode && errNode.attrs && errNode.attrs.code;
|
package/lib/MediaThumbnail.js
CHANGED
|
@@ -290,6 +290,70 @@ async function imageThumbnail(buf, opts) {
|
|
|
290
290
|
return out;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
// ─── Profile pictures ────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
// What WhatsApp's own apps upload. The server will not take an arbitrary file:
|
|
296
|
+
// it wants a square JPEG at this size, and a picture that is not one is dropped
|
|
297
|
+
// without an answer rather than refused, so the failure gives no clue.
|
|
298
|
+
const AVATAR_SIZE = 640;
|
|
299
|
+
const AVATAR_QUALITY = 50;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Re-encode an image into the form a profile picture has to be in.
|
|
303
|
+
*
|
|
304
|
+
* Cropped square from the top-left of the shorter side, scaled to 640x640, and
|
|
305
|
+
* written out as a fresh JPEG. Re-encoding is the point rather than a side
|
|
306
|
+
* effect: it strips EXIF, progressive scans and unusual chroma subsampling —
|
|
307
|
+
* anything the file happened to carry — and produces a plain baseline JPEG.
|
|
308
|
+
*
|
|
309
|
+
* Returns the original buffer untouched when no image library is installed,
|
|
310
|
+
* which at least gives an already-correct picture a chance to go through.
|
|
311
|
+
*/
|
|
312
|
+
async function prepareProfilePicture(buf, opts) {
|
|
313
|
+
opts = opts || {};
|
|
314
|
+
if (!Buffer.isBuffer(buf) || !buf.length) return buf;
|
|
315
|
+
|
|
316
|
+
const size = opts.size || AVATAR_SIZE;
|
|
317
|
+
const quality = opts.quality || AVATAR_QUALITY;
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const lib = await loadImageLib();
|
|
321
|
+
|
|
322
|
+
if (lib && lib.kind === 'sharp') {
|
|
323
|
+
return await lib.sharp(buf)
|
|
324
|
+
.resize(size, size, { fit: 'cover' })
|
|
325
|
+
.jpeg({ quality })
|
|
326
|
+
.toBuffer();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (lib && lib.kind === 'jimp1') {
|
|
330
|
+
const img = await lib.Jimp.read(buf);
|
|
331
|
+
const min = Math.min(img.width, img.height);
|
|
332
|
+
const sq = img.crop({ x: 0, y: 0, w: min, h: min });
|
|
333
|
+
const resizeOpts = { w: size, h: size };
|
|
334
|
+
if (lib.ResizeStrategy && lib.ResizeStrategy.BILINEAR) {
|
|
335
|
+
resizeOpts.mode = lib.ResizeStrategy.BILINEAR;
|
|
336
|
+
}
|
|
337
|
+
return await sq.resize(resizeOpts).getBuffer('image/jpeg', { quality });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (lib && lib.kind === 'jimp0') {
|
|
341
|
+
const Jimp = lib.Jimp;
|
|
342
|
+
const img = await Jimp.read(buf);
|
|
343
|
+
const min = Math.min(img.bitmap.width, img.bitmap.height);
|
|
344
|
+
img.crop(0, 0, min, min).resize(size, size).quality(quality);
|
|
345
|
+
return await img.getBufferAsync(Jimp.MIME_JPEG);
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
_whaDbg('[DBG] AVATAR could not re-encode: ' + (err && err.message));
|
|
349
|
+
return buf;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_whaDbg('[DBG] AVATAR no image library — sending the file as it is. ' +
|
|
353
|
+
'WhatsApp wants a square JPEG; install jimp to have that done for you.');
|
|
354
|
+
return buf;
|
|
355
|
+
}
|
|
356
|
+
|
|
293
357
|
// ─── ffmpeg ──────────────────────────────────────────────────────────────────
|
|
294
358
|
|
|
295
359
|
function run(cmd, args) {
|
|
@@ -385,8 +449,11 @@ module.exports = {
|
|
|
385
449
|
probeImageSize,
|
|
386
450
|
extractExifThumbnail,
|
|
387
451
|
imageThumbnail,
|
|
452
|
+
prepareProfilePicture,
|
|
388
453
|
videoThumbnail,
|
|
389
454
|
hasFfmpeg,
|
|
390
455
|
THUMB_WIDTH,
|
|
391
|
-
THUMB_QUALITY
|
|
456
|
+
THUMB_QUALITY,
|
|
457
|
+
AVATAR_SIZE,
|
|
458
|
+
AVATAR_QUALITY
|
|
392
459
|
};
|