plogme 1.0.5 → 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.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  npm install plogme
11
11
  ```
12
12
 
13
- The current package version is **1.0.4**. Use Node.js 20 or newer.
13
+ The current package version is **1.0.6**. Use Node.js 20 or newer.
14
14
 
15
15
  ## Quick start
16
16
 
@@ -474,6 +474,93 @@ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
474
474
  })
475
475
  ```
476
476
 
477
+ ## Reporting, group history, rich HTML, and source grep
478
+
479
+ Version 1.0.6 adds the realvare-style `w:report` methods without replacing plogme's existing evidence-based `reportSpam` method. The methods use different WhatsApp IQ namespaces and should be selected according to the operation you intend to perform.
480
+
481
+ | API | Wire operation | Use |
482
+ | --- | --- | --- |
483
+ | `reportSpam(jid, messages, spamFlow, subject)` | `xmlns="spam"` → `spam_list` | Report a chat or group with message evidence. |
484
+ | `reportUser(jid, reason, waitForAckMs)` | `xmlns="w:report"` → `user` | Report one user. |
485
+ | `reportMessage(jid, messageId, reason, participantJid, waitForAckMs)` | `xmlns="w:report"` → `message` | Report one message. |
486
+ | `reportAndBlockUser(jid, reason, waitForAckMs)` | `w:report` followed by block | Report a user and then block them. |
487
+
488
+ ```js
489
+ // Existing plogme spam-list report with evidence.
490
+ await sock.reportSpam(
491
+ '1203630@g.us',
492
+ [{ id: 'ABCD1234', from: '15550001111@s.whatsapp.net', t: 1710000000 }],
493
+ 'group_info_report',
494
+ 'Example group'
495
+ )
496
+
497
+ // New direct report methods.
498
+ const acknowledged = await sock.reportUser('15550001111@s.whatsapp.net', 'spam')
499
+ const messageAcknowledged = await sock.reportMessage(
500
+ '15550001111@s.whatsapp.net', 'ABCD1234', 'harassment',
501
+ '15550001111@s.whatsapp.net'
502
+ )
503
+ await sock.reportAndBlockUser('15550001111@s.whatsapp.net', 'scam')
504
+ ```
505
+
506
+ `reportUser` and `reportMessage` return `true` when WhatsApp acknowledges the IQ request and `false` when it times out. A timeout does not prove delivery failed; the request may already have reached the server.
507
+
508
+ ### Group history sharing
509
+
510
+ `toggleGroupHistory(jid, true)` selects `ALL_MEMBER_SHARE`; passing `false` selects `ADMIN_SHARE` through the WhatsApp MEX endpoint:
511
+
512
+ ```js
513
+ await sock.toggleGroupHistory('1203630@g.us', true)
514
+ await sock.toggleGroupHistory('1203630@g.us', false)
515
+ ```
516
+
517
+ ### Two HTML transports
518
+
519
+ `sendHtmlMessage` sends the ordinary single-screen `FOAHtmlPrimitiveDemoDONOTUSE` envelope. `sendRichHtmlMessage` is the reusable transport for interactive GenAI HTML screens like tabbed games; it uses `GenAIaeacdsnwHtmlPrimitive` inside `FOAIDNixelButtonSheets` and supports multiple tabs.
520
+
521
+ ```js
522
+ await sock.sendHtmlMessage(jid, {
523
+ html: '<h1>Simple screen</h1><p>One HTML payload.</p>',
524
+ trustedSources: []
525
+ })
526
+
527
+ await sock.sendRichHtmlMessage(jid, {
528
+ title: 'Games',
529
+ url: 'https://your-domain.example',
530
+ trustedSources: ['your-domain.example'],
531
+ tabs: [
532
+ { tab_header: 'mini-app', html: '<canvas id="game"></canvas><script>/* game */</script>' },
533
+ { tab_header: 'Instructions', html: '<h2>How to play</h2><p>Tap to jump.</p>' }
534
+ ]
535
+ })
536
+ ```
537
+
538
+ The rich transport is deliberately separate because clients that render the normal HTML primitive do not necessarily open the embedded-screen/tabbed envelope the same way.
539
+
540
+ The built-in `sendSlotMachine` keeps its existing ordinary HTML transport. The attached interactive mini-app is available as `sendMiniApp`; because it uses the embedded-screen envelope, plogme configures that built-in game with the owned `pair.crysnova.link` source:
541
+
542
+ ```js
543
+ await sock.sendMiniApp(jid)
544
+ await sock.sendMiniApp(jid, { title: 'My mini-app' })
545
+ ```
546
+
547
+ The `sendRichHtmlMessage` function itself remains neutral: user-supplied screens do not inherit `pair.crysnova.link` and should provide their own `url` and `trustedSources` when those fields are needed.
548
+
549
+ ### Reusable source grep
550
+
551
+ `grepSource` is exported from `lib/Utils/index.js`, and `sock.grepCode` is available after socket creation. It returns exact relative file paths, one-based line numbers, columns, and matching source lines without invoking a shell.
552
+
553
+ ```js
554
+ const matches = sock.grepCode('sendRichHtmlMessage', { maxResults: 20 })
555
+ console.log(matches)
556
+ // [{ file: 'lib/Socket/messages-send.js', line: 2705, column: 12, text: '...' }]
557
+
558
+ const reportMatches = sock.grepCode('reportMessage', { caseSensitive: false })
559
+ const regexMatches = sock.grepCode('report(User|Message)', { regex: true, flags: 'i' })
560
+ ```
561
+
562
+ By default the search covers `lib` and `WAProto`; `includeReadme: true` also searches the README.
563
+
477
564
  ## Development
478
565
 
479
566
  ```bash
@@ -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
 
@@ -2661,6 +2699,38 @@ export const makeMessagesSocket = (config) => {
2661
2699
  return fullMsg;
2662
2700
  };
2663
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
+
2664
2734
 
2665
2735
  /**
2666
2736
  * Sends a slot machine game — Fruit Bonanza!
@@ -2686,6 +2756,17 @@ export const makeMessagesSocket = (config) => {
2686
2756
  return socket.sendHtmlMessage(jid, { html }, sendOptions);
2687
2757
  };
2688
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
+
2689
2770
 
2690
2771
  /**
2691
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 = {
@@ -1358,21 +1494,18 @@ export const generateWAMessageContent = async (message, options) => {
1358
1494
  const messageType = Object.keys(m)[0];
1359
1495
  const key = m[messageType];
1360
1496
  const content = message.externalAdReply;
1361
- if ('thumbnail' in content && !Buffer.isBuffer(content.thumbnail)) {
1497
+ if ('thumbnail' in content && content.thumbnail != null && !Buffer.isBuffer(content.thumbnail) && !(content.thumbnail instanceof Uint8Array)) {
1362
1498
  throw new Boom('Thumbnail must in buffer type', { statusCode: 400 });
1363
1499
  }
1364
- if (!content.url || typeof content.url !== 'string') {
1365
- content.url = DONATE_URL; // Lia@Note 02-02-26 --- Apologies if this feels cheeky, just a fallback
1366
- }
1367
1500
  const externalAdReply = {
1368
1501
  ...content,
1369
1502
  body: content.body,
1370
- mediaType: content.mediaType || 1,
1371
- mediaUrl: content.url,
1372
- renderLargerThumbnail: content.largeThumbnail,
1373
- 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,
1374
1507
  thumbnail: content.thumbnail,
1375
- thumbnailUrl: content.url + '?update=' + Date.now(),
1508
+ thumbnailUrl: content.thumbnailUrl || content.originalImageUrl || (content.url ? content.url + '?update=' + Date.now() : undefined),
1376
1509
  title: content.title || LIBRARY_NAME
1377
1510
  };
1378
1511
  delete externalAdReply.subTitle;
@@ -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: {
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
6
+ const DEFAULT_DIRS = ['lib', 'WAProto'];
7
+ const DEFAULT_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.d.ts', '.proto', '.md', '.json']);
8
+
9
+ const walk = (directory, files) => {
10
+ if (!fs.existsSync(directory)) return;
11
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
12
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
13
+ const fullPath = path.join(directory, entry.name);
14
+ if (entry.isDirectory()) walk(fullPath, files);
15
+ else if (DEFAULT_EXTENSIONS.has(path.extname(entry.name))) files.push(fullPath);
16
+ }
17
+ };
18
+
19
+ /**
20
+ * Find a literal or regular-expression pattern in package source.
21
+ * Returns exact relative file, line, column, and matching source line.
22
+ */
23
+ export const grepSource = (pattern, options = {}) => {
24
+ if (typeof pattern !== 'string' || !pattern.trim()) {
25
+ throw new TypeError('grepSource requires a non-empty search string');
26
+ }
27
+ const expression = options.regex ? new RegExp(pattern, options.flags || 'i') : null;
28
+ const needle = options.caseSensitive ? pattern : pattern.toLowerCase();
29
+ const files = [];
30
+ for (const relativeDir of options.dirs || DEFAULT_DIRS) walk(path.resolve(PACKAGE_ROOT, relativeDir), files);
31
+ if (options.includeReadme) files.push(path.resolve(PACKAGE_ROOT, 'README.md'));
32
+ const matches = [];
33
+ for (const file of files.sort()) {
34
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
35
+ lines.forEach((line, index) => {
36
+ const found = expression ? expression.test(line) : (options.caseSensitive ? line.includes(pattern) : line.toLowerCase().includes(needle));
37
+ if (found) {
38
+ const column = expression ? Math.max(0, line.search(expression)) : (options.caseSensitive ? line.indexOf(pattern) : line.toLowerCase().indexOf(needle));
39
+ matches.push({ file: path.relative(PACKAGE_ROOT, file), line: index + 1, column: column + 1, text: line.trim() });
40
+ }
41
+ if (expression) expression.lastIndex = 0;
42
+ });
43
+ }
44
+ return matches.slice(0, Math.max(1, options.maxResults || 200));
45
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plogme",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Production-ready Baileys fork \u2689 with AI groups, Meta AI prompting, safe compositing fallbacks, rich responses, interactive messages, albums, newsletters, and communities \u2014 without antiban wrappers.",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",