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 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 that is
2169
- not a JPEG it will take, `403` when you are not an admin of that group.
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 of JPEG bytes, or null to remove the current picture.
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 picture is not addressed by sending the IQ *to* yourself: it goes to
4337
- // the server with the account named in `target`, the same shape a group
4338
- // picture uses. Addressed the other way the server files it nowhere and never
4339
- // answers, which looked from the outside like the picture had been set — the
4340
- // stanza went out, nothing came back, and nothing checked.
4341
- async changeProfilePicture(buf) {
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
- const selfJid = this._ownDeviceJid
4344
- ? toNonAdJid(this._ownDeviceJid())
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: this._genMsgId(),
4349
- xmlns: 'w:profile:picture',
4350
- to: 's.whatsapp.net',
4351
- target: jidStrToObj(selfJid),
4352
- type: 'set'
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) throw new Error('changeProfilePicture: no reply from server (IQ timed out)');
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
- async changeGroupPicture(groupJid, buf) {
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
- }, buf ? [new BinaryNode('picture', { type: 'image' }, buf)] : null);
5953
+ }, picture ? [new BinaryNode('picture', { type: 'image' }, picture)] : null);
5935
5954
  const resp = await this._sendIq(node);
5936
- if (!resp) throw new Error('changeGroupPicture: no reply from server (IQ timed out)');
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;
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.10.6",
3
+ "version": "5.10.8",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",