plogme 1.0.4 → 1.0.6

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.
@@ -2,7 +2,7 @@ import { proto } from '../../WAProto/index.js';
2
2
  import { makeLibSignalRepository } from '../Signal/libsignal.js';
3
3
  import { Browsers } from '../Utils/browser-utils.js';
4
4
  import logger from '../Utils/logger.js';
5
- const version = [2, 3000, 1040735178];
5
+ const version = [2, 3000, 1047412487];
6
6
  export const UNAUTHORIZED_CODES = [401, 403, 419];
7
7
  export const BIZ_BOT_SUPPORT_PAYLOAD = '{"version":1,"is_ai_message":true,"should_upload_client_logs":false,"should_show_system_message":false,"ticket_id":"7004947587700716","citation_items":[],"ticket_locale":"us"}';
8
8
  export const DEFAULT_ORIGIN = 'https://web.whatsapp.com';
@@ -325,6 +325,79 @@ export const makeChatsSocket = (config) => {
325
325
  ]
326
326
  });
327
327
  };
328
+ /** Report a user through WhatsApp's w:report IQ endpoint. */
329
+ const reportUser = async (jid, reason = 'spam', waitForAckMs = 5000) => {
330
+ const resolvedJid = jidNormalizedUser(jid);
331
+ const reasonCodes = { spam: 1, harassment: 2, impersonation: 3, scam: 4, other: 5 };
332
+ const node = {
333
+ tag: 'iq',
334
+ attrs: { id: generateMessageTag(), xmlns: 'w:report', to: S_WHATSAPP_NET.replace(/^@/, ''), type: 'set' },
335
+ content: [{ tag: 'user', attrs: { jid: resolvedJid, reason: String(reasonCodes[reason] || 1) } }]
336
+ };
337
+ try {
338
+ await query(node, waitForAckMs);
339
+ logger.info({ jid: resolvedJid, reason }, 'reported user (ack received)');
340
+ return true;
341
+ }
342
+ catch (error) {
343
+ const statusCode = error?.output?.statusCode || error?.statusCode;
344
+ if (statusCode === 408 || error?.message?.toLowerCase?.().includes('timed out')) {
345
+ logger.warn({ jid: resolvedJid, reason }, 'reported user (no ack; request likely sent)');
346
+ return false;
347
+ }
348
+ throw error;
349
+ }
350
+ };
351
+ /** Report one message through WhatsApp's w:report IQ endpoint. */
352
+ const reportMessage = async (jid, messageId, reason = 'spam', participantJid, waitForAckMs = 5000) => {
353
+ const resolvedJid = jidNormalizedUser(jid);
354
+ const participant = participantJid ? jidNormalizedUser(participantJid) : undefined;
355
+ const reasonCodes = { spam: 1, harassment: 2, impersonation: 3, scam: 4, other: 5 };
356
+ const attrs = { id: String(messageId), reason: String(reasonCodes[reason] || 1) };
357
+ if (participant) attrs.participant = participant;
358
+ const node = {
359
+ tag: 'iq',
360
+ attrs: { id: generateMessageTag(), xmlns: 'w:report', to: S_WHATSAPP_NET.replace(/^@/, ''), type: 'set' },
361
+ content: [{ tag: 'message', attrs }]
362
+ };
363
+ try {
364
+ await query(node, waitForAckMs);
365
+ logger.info({ jid: resolvedJid, messageId, reason, participant }, 'reported message (ack received)');
366
+ return true;
367
+ }
368
+ catch (error) {
369
+ const statusCode = error?.output?.statusCode || error?.statusCode;
370
+ if (statusCode === 408 || error?.message?.toLowerCase?.().includes('timed out')) {
371
+ logger.warn({ jid: resolvedJid, messageId, reason, participant }, 'reported message (no ack; request likely sent)');
372
+ return false;
373
+ }
374
+ throw error;
375
+ }
376
+ };
377
+ const reportAndBlockUser = async (jid, reason = 'spam', waitForAckMs = 5000) => {
378
+ const resolvedJid = jidNormalizedUser(jid);
379
+ const acknowledged = await reportUser(resolvedJid, reason, waitForAckMs);
380
+ await updateBlockStatus(resolvedJid, 'block');
381
+ return acknowledged;
382
+ };
383
+ /** Toggle group member-share-history mode through the MEX endpoint. */
384
+ const toggleGroupHistory = async (jid, enable = true) => {
385
+ if (!jid) throw new Boom('toggleGroupHistory requires a group JID', { statusCode: 400 });
386
+ return query({
387
+ tag: 'iq',
388
+ attrs: { type: 'get', to: S_WHATSAPP_NET, xmlns: 'w:mex' },
389
+ content: [{
390
+ tag: 'query',
391
+ attrs: { query_id: '24688994337458819' },
392
+ content: Buffer.from(JSON.stringify({
393
+ variables: {
394
+ group_id: jid,
395
+ input: { member_share_group_history_mode: enable ? 'ALL_MEMBER_SHARE' : 'ADMIN_SHARE' }
396
+ }
397
+ }), 'utf-8')
398
+ }]
399
+ });
400
+ };
328
401
  /**
329
402
  * Get the account opt-out list (`optoutlist` IQ). Returns the raw result node.
330
403
  */
@@ -5,8 +5,9 @@ import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
6
  import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, buildLinkPreview, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, makeWhatsAppFlowButton, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { getUrlInfo } from '../Utils/link-preview.js';
8
- import { prepareRichMenuMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareHtmlMessage } from '../Utils/rich-message-utils.js';
8
+ import { prepareRichMenuMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareHtmlMessage, prepareRichHtmlMessage } from '../Utils/rich-message-utils.js';
9
9
  import { generateSlotMachineHtml } from '../Utils/games/slot-machine.js';
10
+ import { generateMiniAppHtml } from '../Utils/games/miniapp.js';
10
11
  import { generateWebsitePreviewHtml, generateLinkPreviewHtml } from '../Utils/games/website-preview.js';
11
12
  import { isViewOnceMessage, extractViewOnceContent, getViewOnceMediaType, getViewOnceMediaContent, downloadViewOnceMedia, createRegularMessageFromViewOnce, parseViewOnceInfo } from '../Utils/view-once-utils.js';
12
13
  import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
@@ -18,6 +19,7 @@ import { captureUnifiedResponse, generateCodeBlockContent, generateLatexContent,
18
19
  import { Baron } from './interactive-handler.js';
19
20
  import { collectMetaAIBotParticipantJids, isMetaAIBotResponse, META_AI_BOT_JID } from './aigroups.js';
20
21
  import { makeUsernameSocket } from './username.js';
22
+ import { grepSource } from '../Utils/source-grep.js';
21
23
 
22
24
  const STATUS_JID = 'status@broadcast';
23
25
  export const normalizeStatusJidList = (value) => {
@@ -1294,8 +1296,40 @@ export const makeMessagesSocket = (config) => {
1294
1296
  await relayMessage(jid, message, { messageId });
1295
1297
  return { message, messageId };
1296
1298
  },
1297
- sendLatex: async (jid, quoted, options) => {
1298
- const { message, messageId } = generateLatexContent(quoted, options);
1299
+ sendCarousel: async (jid, cards, options = {}) => {
1300
+ if (!Array.isArray(cards) || cards.length === 0) {
1301
+ throw new Boom('sendCarousel requires a non-empty cards array', { statusCode: 400 });
1302
+ }
1303
+ const normalizedCards = cards.map(card => ({
1304
+ ...card,
1305
+ text: card.text ?? card.body?.text,
1306
+ caption: card.caption ?? card.body?.text,
1307
+ footer: card.footer?.text ?? card.footer,
1308
+ buttons: card.buttons ?? card.nativeFlowMessage?.buttons,
1309
+ ...(card.media?.image ? { image: card.media.image } : {}),
1310
+ ...(card.media?.video ? { video: card.media.video } : {}),
1311
+ ...(card.media?.document ? { document: card.media.document } : {})
1312
+ }));
1313
+ const { text, footer, ...messageOptions } = options;
1314
+ const fullMsg = await generateWAMessage(jid, {
1315
+ ...messageOptions,
1316
+ cards: normalizedCards,
1317
+ ...(text !== undefined ? { text } : {}),
1318
+ ...(footer !== undefined ? { footer } : {})
1319
+ }, {
1320
+ logger,
1321
+ userJid,
1322
+ upload: waUploadToServer,
1323
+ mediaCache: config.mediaCache,
1324
+ options: config.options,
1325
+ ...options,
1326
+ messageId: options.messageId || generateMessageIDV2(jid)
1327
+ });
1328
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, ...options });
1329
+ return fullMsg;
1330
+ },
1331
+ sendLatex: async (jid, quoted, options = {}) => {
1332
+ const { message, messageId } = generateLatexContent(quoted, { expressions: [], ...options });
1299
1333
  await relayMessage(jid, message, { messageId });
1300
1334
  return { message, messageId };
1301
1335
  },
@@ -2183,6 +2217,11 @@ export const makeMessagesSocket = (config) => {
2183
2217
  if ('album' in content) {
2184
2218
  const { delayMs = 1500 } = options;
2185
2219
  for (const [messageIndex, albumMedia] of content.album.entries()) {
2220
+ // Give the album collection stanza time to register before
2221
+ // its associated media children are relayed. This mirrors
2222
+ // the ordering used by WhatsApp clients and avoids the
2223
+ // children being treated as unrelated media messages.
2224
+ await delay(delayMs);
2186
2225
  const albumMsg = await generateWAMessage(jid, albumMedia, {
2187
2226
  logger,
2188
2227
  userJid,
@@ -2221,7 +2260,6 @@ export const makeMessagesSocket = (config) => {
2221
2260
  await messageMutex.mutex(() => upsertMessage(albumMsg, 'append'));
2222
2261
  });
2223
2262
  }
2224
- await delay(delayMs);
2225
2263
  }
2226
2264
  }
2227
2265
 
@@ -2604,6 +2642,29 @@ export const makeMessagesSocket = (config) => {
2604
2642
  socket.getViewOnceInfo = (message) => parseViewOnceInfo(message);
2605
2643
 
2606
2644
 
2645
+ /**
2646
+ * Sends an experimental WhatsApp album. The parent album header and each
2647
+ * image/video child are relayed separately and associated by parent key.
2648
+ * Accepts [{ image: ... }, { video: ... }] or [{ type: 'image', data: ... }].
2649
+ */
2650
+ socket.sendAlbumMessage = async (jid, medias = [], options = {}) => {
2651
+ if (!jid) throw new Boom('sendAlbumMessage requires a target JID', { statusCode: 400 });
2652
+ if (!Array.isArray(medias) || medias.length < 2) {
2653
+ throw new Boom('sendAlbumMessage requires at least two media items', { statusCode: 400 });
2654
+ }
2655
+ const album = medias.map((media, index) => {
2656
+ const normalized = media?.type && media?.data
2657
+ ? { [media.type]: media.data }
2658
+ : { ...media };
2659
+ if (index === 0 && options.caption && !normalized.caption) {
2660
+ normalized.caption = options.caption;
2661
+ }
2662
+ return normalized;
2663
+ });
2664
+ return socket.sendMessage(jid, { album }, options);
2665
+ };
2666
+
2667
+
2607
2668
  /**
2608
2669
  * Sends an HTML message — renders raw HTML inside the chat bubble.
2609
2670
  * Uses FOAHtmlPrimitiveDemoDONOTUSE for full HTML/CSS/JS rendering.
@@ -2638,6 +2699,38 @@ export const makeMessagesSocket = (config) => {
2638
2699
  return fullMsg;
2639
2700
  };
2640
2701
 
2702
+ /**
2703
+ * Sends an interactive GenAI HTML screen with embedded tabs.
2704
+ * This is distinct from sendHtmlMessage(): it uses the aeacdsnw HTML
2705
+ * primitive and FOAIDNixelButtonSheets envelope used by interactive games.
2706
+ *
2707
+ * @param {string} jid - Target JID
2708
+ * @param {Object} content - { html, title?, tabs?, trustedSources?, url? }
2709
+ * @param {Object} options - Additional relay options
2710
+ */
2711
+ socket.sendRichHtmlMessage = async (jid, content = {}, options = {}) => {
2712
+ if (!jid) throw new Boom('sendRichHtmlMessage requires a target JID', { statusCode: 400 });
2713
+ if (!content.html && !(Array.isArray(content.tabs) && content.tabs.length)) {
2714
+ throw new Boom('sendRichHtmlMessage requires html or tabs content', { statusCode: 400 });
2715
+ }
2716
+ const wrapped = prepareRichHtmlMessage(content);
2717
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2718
+ logger,
2719
+ userJid: authState.creds.me?.id,
2720
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2721
+ ...options
2722
+ });
2723
+ await relayMessage(jid, fullMsg.message, {
2724
+ messageId: fullMsg.key.id,
2725
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
2726
+ statusJidList: options.statusJidList
2727
+ });
2728
+ return fullMsg;
2729
+ };
2730
+
2731
+ /** Find a word or expression in the installed package source. */
2732
+ socket.grepCode = (pattern, options = {}) => grepSource(pattern, options);
2733
+
2641
2734
 
2642
2735
  /**
2643
2736
  * Sends a slot machine game — Fruit Bonanza!
@@ -2663,6 +2756,17 @@ export const makeMessagesSocket = (config) => {
2663
2756
  return socket.sendHtmlMessage(jid, { html }, sendOptions);
2664
2757
  };
2665
2758
 
2759
+ /** Sends the attached interactive mini-app through the rich HTML envelope. */
2760
+ socket.sendMiniApp = async (jid, options = {}, sendOptions = {}) => {
2761
+ if (!jid) throw new Boom('sendMiniApp requires a target JID', { statusCode: 400 });
2762
+ return socket.sendRichHtmlMessage(jid, {
2763
+ html: generateMiniAppHtml(options),
2764
+ title: options.title || 'mini-app',
2765
+ url: 'https://pair.crysnova.link',
2766
+ trustedSources: ['pair.crysnova.link']
2767
+ }, sendOptions);
2768
+ };
2769
+
2666
2770
 
2667
2771
  /**
2668
2772
  * Mirrors a website inside the WhatsApp chat bubble.
@@ -0,0 +1,5 @@
1
+ /** Generate the attached interactive mini-app HTML game. */
2
+ export const generateMiniAppHtml = (options = {}) => {
3
+ const html = "<style>*{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}</style>\n<body style=\"margin:0;background:transparent;font-family:Arial,sans-serif;color:#eee;touch-action:manipulation;cursor:pointer\">\n<div style=\"width:100%;max-width:620px;margin:auto;padding:16px;box-sizing:border-box\">\n<div style=\"background:rgba(255,255,255,.06);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid rgba(255,255,255,.15);border-radius:16px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.35)\">\n<div style=\"padding:18px 20px;border-bottom:1px solid rgba(255,255,255,.12);display:flex;justify-content:space-between;align-items:center\">\n<div><div style=\"font-size:11px;letter-spacing:1.5px;color:rgba(255,255,255,.45)\">𝑶𝒓𝒊𝒈𝒊𝒏✦</div><div style=\"font-size:21px;font-weight:bold;color:#fff\">mini-app</div></div>\n<div style=\"text-align:right\"><div id=\"score\" style=\"font-size:18px;font-weight:bold;color:#fff;text-shadow:0 0 10px rgba(108,92,231,.85);transition:transform .15s\">00000</div><div id=\"best\" style=\"font-size:10px;color:rgba(255,255,255,.4);margin-top:2px\">BEST 00000</div></div>\n</div>\n<div style=\"padding:18px\">\n<canvas id=\"game\" width=\"560\" height=\"190\" style=\"width:100%;height:auto;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.12);border-radius:12px;display:block\"></canvas>\n<div id=\"status\" style=\"text-align:center;margin-top:10px;font-size:12px;color:rgba(255,255,255,.55)\">Velocità 5.0x</div>\n</div></div></div>\n<script>\nconst c=document.getElementById('game'),x=c.getContext('2d'),scoreEl=document.getElementById('score'),bestEl=document.getElementById('best'),statusEl=document.getElementById('status');\nconst GY=170;\nlet d,o,clouds,particles,ambient,trail,score,best=0,speed,gameOver,last,shake,flash,runT,spawnTimer,milestone,squash;\nfunction loadBest(){\nlet vals=[];\ntry{let v=localStorage.getItem('dino_best');if(v)vals.push(parseInt(v,10))}catch(e){}\ntry{let v=sessionStorage.getItem('dino_best');if(v)vals.push(parseInt(v,10))}catch(e){}\ntry{let m=document.cookie.match(/(?:^|;\\s*)dino_best=(\\d+)/);if(m)vals.push(parseInt(m[1],10))}catch(e){}\nreturn vals.length?Math.max(...vals.filter(v=>!isNaN(v))):0\n}\nfunction saveBest(v){\nlet val=String(Math.floor(v));\ntry{localStorage.setItem('dino_best',val)}catch(e){}\ntry{sessionStorage.setItem('dino_best',val)}catch(e){}\ntry{document.cookie='dino_best='+val+';max-age=31536000;path=/'}catch(e){}\ntry{\nlet rq=indexedDB.open('dino_db',1);\nrq.onupgradeneeded=()=>{rq.result.createObjectStore('kv')};\nrq.onsuccess=()=>{try{rq.result.transaction('kv','readwrite').objectStore('kv').put(val,'dino_best')}catch(e){}}\n}catch(e){}\n}\nfunction loadBestAsync(cb){\ntry{\nlet rq=indexedDB.open('dino_db',1);\nrq.onupgradeneeded=()=>{rq.result.createObjectStore('kv')};\nrq.onsuccess=()=>{\ntry{\nlet gr=rq.result.transaction('kv','readonly').objectStore('kv').get('dino_best');\ngr.onsuccess=()=>{if(gr.result)cb(parseInt(gr.result,10))}\n}catch(e){}\n}\n}catch(e){}\n}\nbest=loadBest();\nloadBestAsync(v=>{if(!isNaN(v)&&v>best){best=v;bestEl.textContent='BEST '+String(Math.floor(best)).padStart(5,'0')}});\nfunction reset(){\nd={x:55,y:132,w:27,h:30,vy:0,jumping:false};\no=[];\nclouds=[{x:120,y:32,w:44,s:.35},{x:300,y:52,w:60,s:.22},{x:460,y:26,w:36,s:.4},{x:560,y:70,w:50,s:.18}];\nparticles=[];\ntrail=[];\nif(!ambient){ambient=[];for(let i=0;i<18;i++)ambient.push({x:Math.random()*c.width,y:Math.random()*c.height,r:.5+Math.random()*1.5,vx:.1+Math.random()*.3,ph:Math.random()*10})}\nscore=0;speed=5;gameOver=false;last=0;shake=0;flash=0;runT=0;milestone=0;squash=1;\nspawnTimer=70+Math.random()*30;\nbestEl.textContent='BEST '+String(Math.floor(best)).padStart(5,'0');\nstatusEl.textContent='Speed 5.0x'\n}\nfunction burst(px,py,n,col,spd){for(let i=0;i<n;i++)particles.push({x:px,y:py,vx:(Math.random()-.5)*spd,vy:-Math.random()*spd,life:1,col,size:2+Math.random()*2})}\nfunction jumpDino(){\nif(gameOver){reset();return}\nif(!d.jumping){d.jumping=true;d.vy=-13;squash=.7;burst(d.x+13,d.y+30,10,'255,255,255',4)}\n}\nfunction cactus(){\nlet h=24+Math.random()*24;\no.push({x:c.width+20,y:GY-h,w:16+Math.random()*6,h});\nif(Math.random()<.22){o.push({x:c.width+20+34+Math.random()*10,y:GY-(20+Math.random()*18),w:16,h:20+Math.random()*18})}\n}\nfunction hit(a,b){return a.x+4<b.x+b.w&&a.x+a.w-4>b.x&&a.y+4<b.y+b.h&&a.y+a.h>b.y}\nfunction drawTrail(){\ntrail.forEach((p,i)=>{x.fillStyle='rgba(108,92,231,'+(.25*(i/trail.length))+')';x.fillRect(p.x,p.y,27,30)})\n}\nfunction drawDino(){\nx.save();\nlet cx=d.x+13,cy=d.y+30;\nx.translate(cx,cy);\nx.scale(1/squash,squash);\nx.translate(-cx,-cy);\nlet legOff=d.jumping?0:Math.sin(runT*.5)*5;\nx.fillStyle='#eaeaea';\nx.fillRect(d.x,d.y,27,30);\nx.fillRect(d.x+22,d.y+5,13,18);\nx.fillStyle='#6c5ce7';\nx.fillRect(d.x+29,d.y+8,4,4);\nx.fillStyle='#eaeaea';\nx.fillRect(d.x+5,d.y+30,6,8+legOff);\nx.fillRect(d.x+20,d.y+30,6,8-legOff);\nx.restore()\n}\nfunction drawCactus(q){\nx.save();\nx.shadowColor='rgba(255,90,90,.35)';x.shadowBlur=10;\nx.fillStyle='#e17a7a';\nx.fillRect(q.x,q.y,q.w,q.h);\nx.fillRect(q.x-7,q.y+10,7,6);\nx.fillRect(q.x-7,q.y+4,6,12);\nx.fillRect(q.x+q.w,q.y+18,7,6);\nx.fillRect(q.x+q.w+1,q.y+12,6,12);\nx.restore()\n}\nfunction drawParticles(){\nparticles.forEach(p=>{x.fillStyle='rgba('+p.col+','+Math.max(p.life,0)+')';x.fillRect(p.x,p.y,p.size,p.size)})\n}\nfunction drawAmbient(){\nambient.forEach(p=>{let a=.15+Math.sin(runT*.05+p.ph)*.1;x.fillStyle='rgba(180,160,255,'+a+')';x.beginPath();x.arc(p.x,p.y,p.r,0,7);x.fill()})\n}\nfunction draw(){\nx.clearRect(0,0,c.width,c.height);\nx.save();\nif(shake>0)x.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);\ndrawAmbient();\nx.fillStyle='rgba(255,255,255,.35)';\nclouds.forEach(q=>{let b=Math.sin(runT*.03+q.x)*2;x.fillRect(q.x,q.y+b,q.w,5);x.fillRect(q.x+10,q.y+b-5,q.w*.45,10)});\nx.strokeStyle='rgba(255,255,255,.25)';\nx.lineWidth=2;\nx.setLineDash([10,8]);\nx.lineDashOffset=-runT*speed*.6;\nx.beginPath();x.moveTo(0,GY);x.lineTo(c.width,GY);x.stroke();\nx.setLineDash([]);\ndrawTrail();\ndrawDino();\no.forEach(drawCactus);\ndrawParticles();\nif(flash>0){x.fillStyle='rgba(255,60,60,'+(flash*.35)+')';x.fillRect(0,0,c.width,c.height)}\nx.restore();\nif(gameOver){\nx.fillStyle='rgba(15,15,25,.55)';x.fillRect(0,0,c.width,c.height);\nx.fillStyle='#fff';x.textAlign='center';\nx.font='bold 24px Arial';x.fillText('GAME OVER',c.width/2,85);\nx.font='14px Arial';x.fillText('Tocca per giocare di nuovo',c.width/2,112);\nx.textAlign='left'\n}\n}\nfunction loop(t){\nif(!last)last=t;\nlet dt=Math.min((t-last)/16.67,2);\nlast=t;\nrunT+=dt;\nif(!gameOver){\nd.y+=d.vy*dt;d.vy+=.75*dt;\nif(d.y>=132){\nif(d.jumping){burst(d.x+13,GY,10,'255,255,255',3.5);squash=1.35}\nd.y=132;d.vy=0;d.jumping=false\n}\nif(d.jumping)trail.push({x:d.x,y:d.y});\nif(trail.length>6)trail.shift();\nif(!d.jumping)trail.length=0;\nsquash+=(1-squash)*.18*dt;\nif(!d.jumping&&Math.floor(runT)%8===0&&Math.random()<.4)burst(d.x+6,GY-2,1,'255,255,255',1.5);\nambient.forEach(p=>{p.x-=p.vx*dt;if(p.x<-4)p.x=c.width+4});\nspawnTimer-=dt;\nif(spawnTimer<=0){cactus();spawnTimer=Math.max(38,62-speed*1.4)+Math.random()*30}\no.forEach(q=>q.x-=speed*dt);\no=o.filter(q=>q.x>-40);\nclouds.forEach(q=>{q.x-=q.s*dt;if(q.x<-80)q.x=c.width+Math.random()*100});\nparticles.forEach(p=>{p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=.3*dt;p.life-=.03*dt});\nparticles=particles.filter(p=>p.life>0);\nspeed=Math.min(11,speed+.0018*dt);\nscore+=dt*.6;\nif(score>best)best=score;\nif(Math.floor(score/500)>milestone){\nmilestone=Math.floor(score/500);\nscoreEl.style.transform='scale(1.35)';\nsetTimeout(()=>scoreEl.style.transform='scale(1)',150)\n}\nscoreEl.textContent=String(Math.floor(score)).padStart(5,'0');\nbestEl.textContent='BEST '+String(Math.floor(best)).padStart(5,'0');\nstatusEl.textContent='Speed '+speed.toFixed(1)+'x';\nfor(const q of o)if(hit(d,q)){\ngameOver=true;shake=14;flash=1;\nsaveBest(best);\nburst(d.x+13,d.y+15,18,'255,90,90',5)\n}\n}\nif(shake>0)shake=Math.max(0,shake-.6*dt);\nif(flash>0)flash=Math.max(0,flash-.05*dt);\ndraw();\nrequestAnimationFrame(loop)\n}\ndocument.addEventListener('pointerdown',e=>{e.preventDefault();jumpDino()});\ndocument.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();jumpDino()}});\nreset();\nrequestAnimationFrame(loop);\n</script></body>";
4
+ return options.title ? html.replace('mini-app', String(options.title)) : html;
5
+ };
@@ -33,3 +33,5 @@ export * from './browser-utils.js';
33
33
  export * from './companion-reg-client-utils.js';
34
34
  export * from './identity-change-handler.js';
35
35
  export * from './stanza-ack.js';
36
+ export * from './source-grep.js';
37
+ export * from './games/miniapp.js';
@@ -330,8 +330,8 @@ export const generateCodeBlockContent = (code, quoted, options = {}) => {
330
330
  };
331
331
  };
332
332
 
333
- export const generateLatexContent = (quoted, options) => {
334
- const { text, expressions, headerText, footer } = options;
333
+ export const generateLatexContent = (quoted, options = {}) => {
334
+ const { text, expressions = [], headerText, footer } = options;
335
335
  const submessages = [];
336
336
  if (headerText) {
337
337
  submessages.push({ messageType: 2, messageText: headerText });
@@ -8,7 +8,7 @@ import { AssociationType, ButtonHeaderType, ButtonType, CarouselCardType, ListTy
8
8
  import { isLidUser, isPnUser, isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
9
9
  import { sha256 } from './crypto.js';
10
10
  import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
- import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getImageProcessingLibrary, getRawMediaUploadData, getStream, toBuffer } from './messages-media.js';
11
+ import { downloadContentFromMessage, encryptedStream, extractImageThumb, generateThumbnail, getAudioDuration, getAudioWaveform, getImageProcessingLibrary, getRawMediaUploadData, getStream, toBuffer } from './messages-media.js';
12
12
  import { premiumEnabled } from './integrity.js';
13
13
  import { prepareRichResponseMessage } from './rich-message-utils.js';
14
14
  import { shouldIncludeReportingToken } from './reporting-utils.js';
@@ -651,6 +651,48 @@ export const hasValidCarouselHeader = (message) => {
651
651
  };
652
652
  export const generateWAMessageContent = async (message, options) => {
653
653
  var _a, _b;
654
+ const fixupExternalAdReply = async (externalAdReply) => {
655
+ if (!externalAdReply || typeof externalAdReply !== 'object') {
656
+ return externalAdReply;
657
+ }
658
+ const thumbUrl = externalAdReply.originalImageUrl || externalAdReply.thumbnailUrl;
659
+ const currentThumb = externalAdReply.thumbnail;
660
+ const currentThumbLength = currentThumb && typeof currentThumb.length === 'number' ? currentThumb.length : 0;
661
+ if (thumbUrl && (!currentThumb || currentThumbLength < 2000)) {
662
+ try {
663
+ const response = await fetch(thumbUrl, { signal: AbortSignal.timeout(7000) });
664
+ if (response.ok) {
665
+ const downloaded = Buffer.from(await response.arrayBuffer());
666
+ if (downloaded.length) {
667
+ const { buffer } = await extractImageThumb(downloaded, 192);
668
+ if (buffer?.length) {
669
+ externalAdReply.thumbnail = buffer;
670
+ }
671
+ }
672
+ }
673
+ }
674
+ catch (error) {
675
+ options?.logger?.warn?.({ err: error?.message, url: thumbUrl }, 'failed to download externalAdReply thumbnail');
676
+ }
677
+ }
678
+ const thumbnail = externalAdReply.thumbnail;
679
+ const hasThumbnail = !!(thumbnail && (thumbnail.length || thumbnail.byteLength));
680
+ const isPrivateUrl = (value) => typeof value === 'string' && /(?:^|[/.])[a-z0-9-]*\.?whatsapp\.net\//i.test(value);
681
+ if (hasThumbnail) {
682
+ if (isPrivateUrl(externalAdReply.thumbnailUrl)) delete externalAdReply.thumbnailUrl;
683
+ if (isPrivateUrl(externalAdReply.originalImageUrl)) delete externalAdReply.originalImageUrl;
684
+ }
685
+ if (externalAdReply.mediaType == null) externalAdReply.mediaType = hasThumbnail ? 1 : 0;
686
+ if (externalAdReply.showAdAttribution === undefined) externalAdReply.showAdAttribution = false;
687
+ if (externalAdReply.renderLargerThumbnail === undefined) externalAdReply.renderLargerThumbnail = hasThumbnail;
688
+ return externalAdReply;
689
+ };
690
+ if (message.contextInfo?.externalAdReply) {
691
+ message.contextInfo.externalAdReply = await fixupExternalAdReply(message.contextInfo.externalAdReply);
692
+ }
693
+ if (message.externalAdReply) {
694
+ message.externalAdReply = await fixupExternalAdReply(message.externalAdReply);
695
+ }
654
696
  let m = {};
655
697
  // Premium downgrade: on revoked/rebranded copies, strip every feature the
656
698
  // fork added and fall back to plain messaging (or a visible notice).
@@ -957,6 +999,100 @@ export const generateWAMessageContent = async (message, options) => {
957
999
  vote: message.pollUpdate.vote
958
1000
  };
959
1001
  }
1002
+ else if (hasNonNullishProperty(message, 'statusStickerInteraction')) {
1003
+ const interaction = message.statusStickerInteraction;
1004
+ m.statusStickerInteractionMessage = proto.Message.StatusStickerInteractionMessage.fromObject({
1005
+ key: interaction.key,
1006
+ stickerKey: interaction.stickerKey,
1007
+ type: interaction.type ?? proto.Message.StatusStickerInteractionMessage.StatusStickerType.REACTION
1008
+ });
1009
+ }
1010
+ else if (hasNonNullishProperty(message, 'statusQuoted')) {
1011
+ const quoted = message.statusQuoted;
1012
+ m.statusQuotedMessage = proto.Message.StatusQuotedMessage.fromObject({
1013
+ type: quoted.type ?? proto.Message.StatusQuotedMessage.StatusQuotedMessageType.QUESTION_ANSWER,
1014
+ text: quoted.text,
1015
+ thumbnail: quoted.thumbnail,
1016
+ originalStatusId: quoted.originalStatusId
1017
+ });
1018
+ }
1019
+ else if (hasNonNullishProperty(message, 'statusQuestionAnswer')) {
1020
+ m.statusQuestionAnswerMessage = proto.Message.StatusQuestionAnswerMessage.fromObject(message.statusQuestionAnswer);
1021
+ }
1022
+ else if (hasNonNullishProperty(message, 'questionResponse')) {
1023
+ m.questionResponseMessage = proto.Message.QuestionResponseMessage.fromObject(message.questionResponse);
1024
+ }
1025
+ else if (hasNonNullishProperty(message, 'comment')) {
1026
+ const comment = message.comment;
1027
+ m.commentMessage = proto.Message.CommentMessage.fromObject({ message: comment.message, targetMessageKey: comment.targetMessageKey });
1028
+ }
1029
+ else if (hasNonNullishProperty(message, 'statusNotification')) {
1030
+ m.statusNotificationMessage = proto.Message.StatusNotificationMessage.fromObject(message.statusNotification);
1031
+ }
1032
+ else if (hasNonNullishProperty(message, 'stickerSyncRMR')) {
1033
+ const sync = message.stickerSyncRMR;
1034
+ m.stickerSyncRmrMessage = proto.Message.StickerSyncRMRMessage.fromObject({
1035
+ filehash: sync.filehash || sync.fileHash || [],
1036
+ rmrSource: sync.rmrSource,
1037
+ requestTimestamp: sync.requestTimestamp ?? Date.now()
1038
+ });
1039
+ }
1040
+ else if (hasNonNullishProperty(message, 'sendPayment')) {
1041
+ const payment = message.sendPayment;
1042
+ m.sendPaymentMessage = proto.Message.SendPaymentMessage.fromObject({
1043
+ requestMessageKey: payment.requestMessageKey,
1044
+ noteMessage: payment.noteMessage,
1045
+ background: payment.background,
1046
+ transactionData: payment.transactionData
1047
+ });
1048
+ }
1049
+ else if (hasNonNullishProperty(message, 'declinePayment')) {
1050
+ m.declinePaymentRequestMessage = proto.Message.DeclinePaymentRequestMessage.fromObject({ key: message.declinePayment.key || message.declinePayment });
1051
+ }
1052
+ else if (hasNonNullishProperty(message, 'cancelPayment')) {
1053
+ m.cancelPaymentRequestMessage = proto.Message.CancelPaymentRequestMessage.fromObject({ key: message.cancelPayment.key || message.cancelPayment });
1054
+ }
1055
+ else if (hasNonNullishProperty(message, 'scheduledCallEdit')) {
1056
+ m.scheduledCallEditMessage = proto.Message.ScheduledCallEditMessage.fromObject(message.scheduledCallEdit);
1057
+ }
1058
+ else if (hasNonNullishProperty(message, 'secretEncrypted')) {
1059
+ m.secretEncryptedMessage = proto.Message.SecretEncryptedMessage.fromObject(message.secretEncrypted);
1060
+ }
1061
+ else if (hasNonNullishProperty(message, 'encComment')) {
1062
+ m.encCommentMessage = proto.Message.EncCommentMessage.fromObject(message.encComment);
1063
+ }
1064
+ else if (hasNonNullishProperty(message, 'encEventResponse')) {
1065
+ m.encEventResponseMessage = proto.Message.EncEventResponseMessage.fromObject(message.encEventResponse);
1066
+ }
1067
+ else if (hasNonNullishProperty(message, 'messageHistoryBundle')) {
1068
+ const bundle = message.messageHistoryBundle;
1069
+ const media = bundle.media ? await prepareWAMessageMedia({ document: bundle.media }, options) : undefined;
1070
+ const document = media?.documentMessage;
1071
+ m.messageHistoryBundle = proto.Message.MessageHistoryBundle.fromObject({
1072
+ mimetype: bundle.mimetype || document?.mimetype || 'application/octet-stream',
1073
+ fileSha256: bundle.fileSha256 || document?.fileSha256,
1074
+ mediaKey: bundle.mediaKey || document?.mediaKey,
1075
+ fileEncSha256: bundle.fileEncSha256 || document?.fileEncSha256,
1076
+ directPath: bundle.directPath || document?.directPath,
1077
+ mediaKeyTimestamp: bundle.mediaKeyTimestamp || document?.mediaKeyTimestamp,
1078
+ contextInfo: bundle.contextInfo,
1079
+ messageHistoryMetadata: bundle.messageHistoryMetadata
1080
+ });
1081
+ }
1082
+ else if (hasNonNullishProperty(message, 'messageHistoryNotice')) {
1083
+ m.messageHistoryNotice = proto.Message.MessageHistoryNotice.fromObject(message.messageHistoryNotice);
1084
+ }
1085
+ else if (hasNonNullishProperty(message, 'inviteAdmin')) {
1086
+ const invite = message.inviteAdmin;
1087
+ m.newsletterAdminInviteMessage = proto.Message.NewsletterAdminInviteMessage.fromObject({
1088
+ newsletterJid: invite.newsletterJid || invite.jid,
1089
+ newsletterName: invite.newsletterName || invite.subject,
1090
+ jpegThumbnail: invite.jpegThumbnail || invite.thumbnail,
1091
+ caption: invite.caption || invite.text,
1092
+ inviteExpiration: invite.inviteExpiration,
1093
+ contextInfo: invite.contextInfo
1094
+ });
1095
+ }
960
1096
  // Lia@Changes 01-02-26 --- Add payment invite message
961
1097
  else if (hasNonNullishProperty(message, 'paymentInviteServiceType')) {
962
1098
  m.paymentInviteMessage = {
@@ -1016,6 +1152,33 @@ export const generateWAMessageContent = async (message, options) => {
1016
1152
  else if (hasNonNullishProperty(message, 'requestPhoneNumber')) {
1017
1153
  m.requestPhoneNumberMessage = {};
1018
1154
  }
1155
+ else if (hasNonNullishProperty(message, 'chatTheme')) {
1156
+ const theme = message.chatTheme || {};
1157
+ const themeSetting = {};
1158
+ if (theme.animatedWallpaper) {
1159
+ const wallpaper = theme.animatedWallpaper;
1160
+ themeSetting.animatedWallpaper = proto.Message.ChatAnimatedWallpaper.fromObject({
1161
+ animatedWallpaperId: wallpaper.id || wallpaper.animatedWallpaperId,
1162
+ dimLevel: wallpaper.dimLevel ?? (wallpaper.opacity != null ? wallpaper.opacity / 100 : 0)
1163
+ });
1164
+ }
1165
+ if (Object.keys(themeSetting).length) {
1166
+ m.protocolMessage = {
1167
+ type: proto.Message.ProtocolMessage.Type.CHAT_THEME_SETTING,
1168
+ chatThemeSetting: proto.Message.ChatThemeSetting.fromObject(themeSetting)
1169
+ };
1170
+ }
1171
+ }
1172
+ else if (hasNonNullishProperty(message, 'newsletterFollowerInvite') ||
1173
+ hasNonNullishProperty(message, 'newsletterFollowerInviteMessage') ||
1174
+ hasNonNullishProperty(message, 'newsletterFollowerInviteMessageV2')) {
1175
+ const isV2 = hasNonNullishProperty(message, 'newsletterFollowerInviteMessageV2');
1176
+ const invite = isV2
1177
+ ? message.newsletterFollowerInviteMessageV2
1178
+ : (message.newsletterFollowerInvite ?? message.newsletterFollowerInviteMessage);
1179
+ const field = isV2 ? 'newsletterFollowerInviteMessageV2' : 'newsletterFollowerInviteMessage';
1180
+ m = { [field]: proto.Message.NewsletterFollowerInviteMessage.fromObject(invite) };
1181
+ }
1019
1182
  else if (hasNonNullishProperty(message, 'limitSharing')) {
1020
1183
  m.protocolMessage = {
1021
1184
  type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
@@ -1033,6 +1196,12 @@ export const generateWAMessageContent = async (message, options) => {
1033
1196
  m.audioMessage.backgroundArgb = await assertColor(options.backgroundColor);
1034
1197
  }
1035
1198
  }
1199
+ if (m.stickerMessage && hasNonNullishProperty(message, 'stickerAudio')) {
1200
+ const audioPrepared = await prepareWAMessageMedia({ audio: message.stickerAudio, ptt: true }, options);
1201
+ if (audioPrepared?.audioMessage) {
1202
+ m.stickerMessage.audioMessage = audioPrepared.audioMessage;
1203
+ }
1204
+ }
1036
1205
  // Lia@Changes 30-01-26 --- Add interactive messages (buttonsMessage, listMessage, interactiveMessage, templateMessage, and carouselMessage)
1037
1206
  if (hasNonNullishProperty(message, 'buttons')) {
1038
1207
  const buttonsMessage = {
@@ -1325,21 +1494,18 @@ export const generateWAMessageContent = async (message, options) => {
1325
1494
  const messageType = Object.keys(m)[0];
1326
1495
  const key = m[messageType];
1327
1496
  const content = message.externalAdReply;
1328
- if ('thumbnail' in content && !Buffer.isBuffer(content.thumbnail)) {
1497
+ if ('thumbnail' in content && content.thumbnail != null && !Buffer.isBuffer(content.thumbnail) && !(content.thumbnail instanceof Uint8Array)) {
1329
1498
  throw new Boom('Thumbnail must in buffer type', { statusCode: 400 });
1330
1499
  }
1331
- if (!content.url || typeof content.url !== 'string') {
1332
- content.url = DONATE_URL; // Lia@Note 02-02-26 --- Apologies if this feels cheeky, just a fallback
1333
- }
1334
1500
  const externalAdReply = {
1335
1501
  ...content,
1336
1502
  body: content.body,
1337
- mediaType: content.mediaType || 1,
1338
- mediaUrl: content.url,
1339
- renderLargerThumbnail: content.largeThumbnail,
1340
- sourceUrl: content.url,
1503
+ mediaType: content.mediaType ?? (content.thumbnail ? 1 : 0),
1504
+ mediaUrl: content.mediaUrl || content.url,
1505
+ renderLargerThumbnail: content.largeThumbnail ?? content.renderLargerThumbnail ?? !!content.thumbnail,
1506
+ sourceUrl: content.sourceUrl || content.url,
1341
1507
  thumbnail: content.thumbnail,
1342
- thumbnailUrl: content.url + '?update=' + Date.now(),
1508
+ thumbnailUrl: content.thumbnailUrl || content.originalImageUrl || (content.url ? content.url + '?update=' + Date.now() : undefined),
1343
1509
  title: content.title || LIBRARY_NAME
1344
1510
  };
1345
1511
  delete externalAdReply.subTitle;
@@ -138,6 +138,17 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
138
138
  }
139
139
  }
140
140
  const content = normalizeMessageContent(message.message);
141
+ if (content?.acp2SettingMessage) {
142
+ const acp2Setting = message.messageContextInfo?.acp2Setting;
143
+ if (acp2Setting) {
144
+ Object.assign(chat, {
145
+ acp2Enabled: acp2Setting.enabled || false,
146
+ acp2SettingTimestamp: toNumber(acp2Setting.settingTimestamp) || toNumber(message.messageTimestamp),
147
+ acp2Trigger: acp2Setting.trigger,
148
+ acp2InitiatedByMe: acp2Setting.initiatedByMe || false
149
+ });
150
+ }
151
+ }
141
152
  // unarchive chat if it's a real message, or someone reacted to our message
142
153
  // and we've the unarchive chats setting on
143
154
  if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
@@ -579,4 +590,4 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
579
590
  ev.emit('chats.update', [chat]);
580
591
  }
581
592
  };
582
- export default processMessage;
593
+ export default processMessage;
@@ -947,6 +947,65 @@ export const prepareHtmlMessage = (options = {}) => {
947
947
  return wrapToBotForwardedMessage(richResponseMessage);
948
948
  };
949
949
 
950
+ /**
951
+ * Build the alternate GenAI HTML envelope used by interactive HTML screens.
952
+ * Unlike prepareHtmlMessage(), this includes embedded_screens and uses the
953
+ * aeacdsnw HTML primitive used by the attached game message.
954
+ */
955
+ export const prepareRichHtmlMessage = (options = {}) => {
956
+ const {
957
+ html = '',
958
+ trustedSources = [],
959
+ url = '',
960
+ title = 'Preview',
961
+ tabHeader = 'HTML',
962
+ tabs
963
+ } = options;
964
+ const tabItems = Array.isArray(tabs) && tabs.length
965
+ ? tabs
966
+ : [{ id: 'tab_0', tab_header: tabHeader, html }];
967
+ const embeddedTabs = tabItems.map((tab, index) => ({
968
+ id: tab.id || `tab_${index}`,
969
+ tab_header: tab.tab_header || tab.title || `HTML ${index + 1}`,
970
+ sections: [{
971
+ __typename: 'GenAIUnifiedResponseSection',
972
+ view_model: {
973
+ __typename: 'GenAISingleLayoutViewModel',
974
+ primitive: {
975
+ __typename: 'GenAIaeacdsnwHtmlPrimitive',
976
+ payload: String(tab.html ?? tab.payload ?? html).trim(),
977
+ url: tab.url || url,
978
+ trusted_sources: tab.trustedSources || trustedSources
979
+ }
980
+ }
981
+ }],
982
+ step_entries: []
983
+ }));
984
+ const unified = {
985
+ response_id: randomUUID(),
986
+ sections: [{
987
+ view_model: {
988
+ primitive: { text: `> ${title}`, __typename: 'GenAIMarkdownTextUXPrimitive' },
989
+ __typename: 'GenAISingleLayoutViewModel'
990
+ }
991
+ }],
992
+ embedded_screens: [{
993
+ title,
994
+ content: [{
995
+ __typename: 'FOAIDNixelButtonSheets',
996
+ tabs: embeddedTabs
997
+ }]
998
+ }]
999
+ };
1000
+ const richResponseMessage = proto.AIRichResponseMessage.create({
1001
+ messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
1002
+ submessages: [{ messageType: RichSubMessageType.TEXT, messageText: `> ${title}` }],
1003
+ unifiedResponse: { data: Buffer.from(JSON.stringify(unified)) },
1004
+ contextInfo: { isForwarded: true, forwardingScore: 1, forwardOrigin: 4 }
1005
+ });
1006
+ return wrapToBotForwardedMessage(richResponseMessage);
1007
+ };
1008
+
950
1009
  export const wrapToBotForwardedMessage = (richResponseMessage) => ({
951
1010
  messageContextInfo: {
952
1011
  botMetadata: {