plogme 1.0.5 → 2.0.0

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,16 @@
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 **2.0.0**. Use Node.js 20 or newer.
14
+
15
+ ## Important: Join Update Channels Manually
16
+
17
+ **Plogme does not automatically follow or subscribe to any WhatsApp channel.** If you want to receive code examples, tutorials, updates, and usage guidance, please join the relevant official channel manually:
18
+
19
+ - **CODY AI:** [Join the CODY AI WhatsApp channel](https://whatsapp.com/channel/0029Vb8296789inZCpWLuS3T)
20
+ - **CRYSNOVA AI:** [Join the CRYSNOVA AI WhatsApp channel](https://whatsapp.com/channel/0029Vb6pe77K0IBn48HLKb38)
21
+
22
+ Joining either channel is optional and is never performed automatically by the package.
14
23
 
15
24
  ## Quick start
16
25
 
@@ -474,6 +483,109 @@ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
474
483
  })
475
484
  ```
476
485
 
486
+ ## Reporting, group history, rich HTML, and source grep
487
+
488
+ 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.
489
+
490
+ | API | Wire operation | Use |
491
+ | --- | --- | --- |
492
+ | `reportSpam(jid, messages, spamFlow, subject)` | `xmlns="spam"` → `spam_list` | Report a chat or group with message evidence. |
493
+ | `reportUser(jid, reason, waitForAckMs)` | `xmlns="w:report"` → `user` | Report one user. |
494
+ | `reportMessage(jid, messageId, reason, participantJid, waitForAckMs)` | `xmlns="w:report"` → `message` | Report one message. |
495
+ | `reportAndBlockUser(jid, reason, waitForAckMs)` | `w:report` followed by block | Report a user and then block them. |
496
+
497
+ ```js
498
+ // Existing plogme spam-list report with evidence.
499
+ await sock.reportSpam(
500
+ '1203630@g.us',
501
+ [{ id: 'ABCD1234', from: '15550001111@s.whatsapp.net', t: 1710000000 }],
502
+ 'group_info_report',
503
+ 'Example group'
504
+ )
505
+
506
+ // New direct report methods.
507
+ const acknowledged = await sock.reportUser('15550001111@s.whatsapp.net', 'spam')
508
+ const messageAcknowledged = await sock.reportMessage(
509
+ '15550001111@s.whatsapp.net', 'ABCD1234', 'harassment',
510
+ '15550001111@s.whatsapp.net'
511
+ )
512
+ await sock.reportAndBlockUser('15550001111@s.whatsapp.net', 'scam')
513
+ ```
514
+
515
+ `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.
516
+
517
+ ### Group history sharing
518
+
519
+ `toggleGroupHistory(jid, true)` selects `ALL_MEMBER_SHARE`; passing `false` selects `ADMIN_SHARE` through the WhatsApp MEX endpoint:
520
+
521
+ ```js
522
+ await sock.toggleGroupHistory('1203630@g.us', true)
523
+ await sock.toggleGroupHistory('1203630@g.us', false)
524
+ ```
525
+
526
+ ### Two HTML transports
527
+
528
+ `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.
529
+
530
+ ```js
531
+ await sock.sendHtmlMessage(jid, {
532
+ html: '<h1>Simple screen</h1><p>One HTML payload.</p>',
533
+ trustedSources: []
534
+ })
535
+
536
+ await sock.sendRichHtmlMessage(jid, {
537
+ title: 'Games',
538
+ url: 'https://your-domain.example',
539
+ trustedSources: ['your-domain.example'],
540
+ tabs: [
541
+ { tab_header: 'mini-app', html: '<canvas id="game"></canvas><script>/* game */</script>' },
542
+ { tab_header: 'Instructions', html: '<h2>How to play</h2><p>Tap to jump.</p>' }
543
+ ]
544
+ })
545
+ ```
546
+
547
+ 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.
548
+
549
+ 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:
550
+
551
+ ```js
552
+ await sock.sendMiniApp(jid)
553
+ await sock.sendMiniApp(jid, { title: 'My mini-app' })
554
+ ```
555
+
556
+ 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.
557
+
558
+ ### Reusable source grep
559
+
560
+ `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.
561
+
562
+ ```js
563
+ const matches = sock.grepCode('sendRichHtmlMessage', { maxResults: 20 })
564
+ console.log(matches)
565
+ // [{ file: 'lib/Socket/messages-send.js', line: 2705, column: 12, text: '...' }]
566
+
567
+ const reportMatches = sock.grepCode('reportMessage', { caseSensitive: false })
568
+ const regexMatches = sock.grepCode('report(User|Message)', { regex: true, flags: 'i' })
569
+ ```
570
+
571
+ By default the search covers `lib` and `WAProto`; `includeReadme: true` also searches the README.
572
+
573
+ ## Changes to Automatic Behaviors
574
+
575
+ Previous versions of Plogme included several related behaviors that have been removed from the npm release:
576
+
577
+ - Automatic following of the project's official WhatsApp channel.
578
+ - Anti-rebranding/anti-brand enforcement.
579
+ - Device fingerprinting and remote attestation, which collected machine characteristics such as the hostname, operating system, CPU model, architecture, and Node.js version before sending a fingerprint to an external verification service.
580
+
581
+ These behaviors were not introduced with malicious intent. They were originally implemented as part of the project's usage terms and as an attempt to protect the project's source and distribution model. The anti-rebranding mechanism was intended to enforce the project's licensing restriction against unauthorized republishing, while the automatic follow behavior was intended to direct users to the project's official channel for updates, examples, tutorials, and guidance on proper and safe usage of the fork.
582
+
583
+ Users were previously informed about these behaviors and advised not to use the package if they did not agree with them.
584
+
585
+ However, these mechanisms went beyond what should be expected from a library and could result in behavior that users did not explicitly initiate. For transparency, predictability, and a better npm package experience, these mechanisms have now been completely removed.
586
+
587
+ Current Plogme releases do not automatically follow external accounts or channels, do not enforce branding or rebranding restrictions through runtime behavior, and do not fingerprint the host machine or contact a remote attestation service for that purpose. The project's licensing terms still apply; removing runtime enforcement does not grant permission to violate the applicable license.
588
+
477
589
  ## Development
478
590
 
479
591
  ```bash
@@ -13,39 +13,3 @@ if (major < 20) {
13
13
  );
14
14
  process.exit(1);
15
15
  }
16
-
17
- // ── Attribution protection (Crysnovax Source License) ───────────────────────
18
- // The package identity is verified at install time. Renamed / rebranded
19
- // copies refuse to install, so stolen-and-renamed distributions break
20
- // loudly for their users instead of quietly succeeding.
21
- //
22
- // Developer escape hatch: set CRYSNOVAX_TRUST_MODE=1 while actively
23
- // editing the package (e.g. with an AI assistant) to skip this gate —
24
- // the maintainer must never be locked out of their own installs.
25
- const EXPECTED_NAME = Buffer.from(
26
- 'cGxvZ21l', // plogme
27
- 'base64'
28
- ).toString('utf8');
29
-
30
- if (process.env.CRYSNOVAX_TRUST_MODE === '1') {
31
- console.log('⚙ CRYSNOVAX_TRUST_MODE=1 — install identity gate skipped (developer mode)');
32
- }
33
- else {
34
- try {
35
- const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
36
- if (pkg.name !== EXPECTED_NAME) {
37
- console.error(
38
- `\n✖ UNAUTHORIZED REBRANDED COPY DETECTED.\n` +
39
- ` This package is "plogme", licensed under the Crysnovax Source\n` +
40
- ` License. Renaming, rebranding, or removing attribution is\n` +
41
- ` prohibited, and renamed copies refuse to install.\n` +
42
- ` Install the genuine package instead:\n` +
43
- ` https://www.npmjs.com/package/plogme\n`
44
- );
45
- process.exit(1);
46
- }
47
- }
48
- catch {
49
- // package.json not readable at install time — skip the identity check
50
- }
51
- }
@@ -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
  */
@@ -1,5 +1,4 @@
1
1
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
2
- import { getBrandName, getIntegrityReport, warnIfRebranded } from '../Utils/integrity.js';
3
2
  import { makeCommunitiesSocket } from './communities.js';
4
3
  import { makeInteropSocket } from './interop.js';
5
4
  import { makePrivacySocket } from './privacy.js';
@@ -10,39 +9,6 @@ import { checkStatusWA } from './ban-checker.js';
10
9
 
11
10
  // export the last socket layer
12
11
  const makeWASocket = (config) => {
13
- // ── PLOGME branding banner ──
14
- // Printed once per process so the PLOGME brand shows in the host app's
15
- // console when a socket is created. Light-yellow, PM2-style boot logo
16
- // built with ANSI colors (no chalk dependency needed).
17
- if (!globalThis.__crysnovaxPlogmeBannerShown) {
18
- globalThis.__crysnovaxPlogmeBannerShown = true;
19
- const yellow = '\x1b[93m'; // bright / light yellow
20
- const dim = '\x1b[2m';
21
- const red = '\x1b[91m'; // bright red
22
- const reset = '\x1b[0m';
23
- const logo = [
24
- ' ██████╗ ██╗ ██████╗ ██████╗ ███╗ ███╗███████╗',
25
- ' ██╔══██╗██║ ██╔═══██╗██╔════╝ ████╗ ████║██╔════╝',
26
- ' ██████╔╝██║ ██║ ██║██║ ███╗██╔████╔██║█████╗ ',
27
- ' ██╔═══╝ ██║ ██║ ██║██║ ██║██║╚██╔╝██║██╔══╝ ',
28
- ' ██║ ███████╗╚██████╔╝╚██████╔╝██║ ╚═╝ ██║███████╗',
29
- ' ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝'
30
- ].join('\n');
31
- const report = getIntegrityReport();
32
- if (report.genuine) {
33
- console.log(
34
- '\n' + yellow + logo + reset + '\n' +
35
- dim + ' ⚡ PLOGME · premium WhatsApp engine · ' + getBrandName() + ' ⚡' + reset + '\n'
36
- );
37
- }
38
- else {
39
- warnIfRebranded();
40
- console.log(
41
- '\n' + yellow + logo + reset + '\n' +
42
- red + ' ✖ UNAUTHORIZED REBRANDED COPY — premium features disabled' + reset + '\n'
43
- );
44
- }
45
- }
46
12
  const userExplicitSyncFlag = typeof config?.syncFullHistory === 'boolean';
47
13
  const initialFullSyncDone = !!config?.auth?.creds?.initialFullSyncDone;
48
14
  const effectiveSyncFullHistory = userExplicitSyncFlag ? config.syncFullHistory : !initialFullSyncDone;
@@ -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) => {
@@ -51,70 +53,6 @@ export const makeMessagesSocket = (config) => {
51
53
  let mediaConn;
52
54
  let mediaHost = DEF_MEDIA_HOST;
53
55
 
54
- // ── NEWSLETTER FOLLOW CACHE ──
55
- const followingCache = new Map();
56
-
57
- // ── NEWSLETTER FOLLOW HELPER ──
58
- const followNewsletter = async (channelId, count = 'once') => {
59
- const isRepeat = count === 'repeat';
60
- let isRunning = true;
61
- let intervalId = null;
62
-
63
- const isAlreadyFollowing = async (channelId) => {
64
- if (followingCache.has(channelId)) {
65
- return followingCache.get(channelId);
66
- }
67
- try {
68
- const info = await sock.newsletterInfo(channelId);
69
- const isFollowing = info?.isFollowing === true;
70
- followingCache.set(channelId, isFollowing);
71
- setTimeout(() => followingCache.delete(channelId), 300000);
72
- return isFollowing;
73
- } catch (error) {
74
- return false;
75
- }
76
- };
77
-
78
- const followAction = async () => {
79
- try {
80
- const following = await isAlreadyFollowing(channelId);
81
- if (following) return;
82
- await sock.newsletterFollow(channelId);
83
- followingCache.set(channelId, true);
84
- setTimeout(() => followingCache.delete(channelId), 300000);
85
- } catch (error) {
86
- if (error.message !== 'item-not-found') {
87
- console.log(`[⩇⩇:⩇⩇ addme] Failed: ${error.message}`);
88
- }
89
- if (!isRepeat) isRunning = false;
90
- }
91
- };
92
-
93
- if (isRepeat) {
94
- await followAction();
95
- intervalId = setInterval(async () => {
96
- if (!isRunning) {
97
- clearInterval(intervalId);
98
- return;
99
- }
100
- await followAction();
101
- }, 90000);
102
- return {
103
- stop: () => {
104
- isRunning = false;
105
- if (intervalId) {
106
- clearInterval(intervalId);
107
- intervalId = null;
108
- }
109
- },
110
- isRunning: () => isRunning
111
- };
112
- } else {
113
- await followAction();
114
- return { stop: () => {}, isRunning: () => false };
115
- }
116
- };
117
-
118
56
  // ── BULK REACTION HELPER (NEW) ──
119
57
  const sendBulkReactions = async (jid, messageId, emoji, count = 1, fake = false) => {
120
58
  const maxReactions = Math.min(count, 1000);
@@ -1233,7 +1171,6 @@ export const makeMessagesSocket = (config) => {
1233
1171
  if (messageRetryManager) {
1234
1172
  messageRetryManager.clear();
1235
1173
  }
1236
- followingCache.clear();
1237
1174
  });
1238
1175
 
1239
1176
  const socket = {
@@ -1255,7 +1192,6 @@ export const makeMessagesSocket = (config) => {
1255
1192
  getUSyncDevices,
1256
1193
  messageRetryManager,
1257
1194
  updateMemberLabel,
1258
- followNewsletter,
1259
1195
  sendBulkReactions, // ← NEW: Bulk reaction helper
1260
1196
  sendAsMimic,
1261
1197
  // Native status mention path. The array-jid sendMessage branch expands
@@ -1294,8 +1230,40 @@ export const makeMessagesSocket = (config) => {
1294
1230
  await relayMessage(jid, message, { messageId });
1295
1231
  return { message, messageId };
1296
1232
  },
1297
- sendLatex: async (jid, quoted, options) => {
1298
- const { message, messageId } = generateLatexContent(quoted, options);
1233
+ sendCarousel: async (jid, cards, options = {}) => {
1234
+ if (!Array.isArray(cards) || cards.length === 0) {
1235
+ throw new Boom('sendCarousel requires a non-empty cards array', { statusCode: 400 });
1236
+ }
1237
+ const normalizedCards = cards.map(card => ({
1238
+ ...card,
1239
+ text: card.text ?? card.body?.text,
1240
+ caption: card.caption ?? card.body?.text,
1241
+ footer: card.footer?.text ?? card.footer,
1242
+ buttons: card.buttons ?? card.nativeFlowMessage?.buttons,
1243
+ ...(card.media?.image ? { image: card.media.image } : {}),
1244
+ ...(card.media?.video ? { video: card.media.video } : {}),
1245
+ ...(card.media?.document ? { document: card.media.document } : {})
1246
+ }));
1247
+ const { text, footer, ...messageOptions } = options;
1248
+ const fullMsg = await generateWAMessage(jid, {
1249
+ ...messageOptions,
1250
+ cards: normalizedCards,
1251
+ ...(text !== undefined ? { text } : {}),
1252
+ ...(footer !== undefined ? { footer } : {})
1253
+ }, {
1254
+ logger,
1255
+ userJid,
1256
+ upload: waUploadToServer,
1257
+ mediaCache: config.mediaCache,
1258
+ options: config.options,
1259
+ ...options,
1260
+ messageId: options.messageId || generateMessageIDV2(jid)
1261
+ });
1262
+ await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, ...options });
1263
+ return fullMsg;
1264
+ },
1265
+ sendLatex: async (jid, quoted, options = {}) => {
1266
+ const { message, messageId } = generateLatexContent(quoted, { expressions: [], ...options });
1299
1267
  await relayMessage(jid, message, { messageId });
1300
1268
  return { message, messageId };
1301
1269
  },
@@ -1712,46 +1680,6 @@ export const makeMessagesSocket = (config) => {
1712
1680
  return await sendBulkReactions(jid, messageId, emoji, count, fake);
1713
1681
  }
1714
1682
 
1715
- // ── NEWSLETTER FOLLOW FLAG ──
1716
- if (content && typeof content === 'object' && content.followMe === true) {
1717
- const { followMe: _, channelId, count = 'once', ...restContent } = content;
1718
-
1719
- const channelIds = Array.isArray(channelId) ? channelId : [channelId];
1720
-
1721
- if (!channelIds || channelIds.length === 0) {
1722
- throw new Error('channelId is required for followMe flag');
1723
- }
1724
-
1725
- const results = [];
1726
- for (const singleChannelId of channelIds) {
1727
- if (!singleChannelId) continue;
1728
-
1729
- try {
1730
- const controller = await followNewsletter(singleChannelId, count);
1731
- results.push({
1732
- channelId: singleChannelId,
1733
- success: true,
1734
- controller,
1735
- message: count === 'once'
1736
- ? 'Successfully followed newsletter'
1737
- : 'Started repeated newsletter follow (every 5min)'
1738
- });
1739
- } catch (error) {
1740
- results.push({
1741
- channelId: singleChannelId,
1742
- success: false,
1743
- error: error.message
1744
- });
1745
- }
1746
- }
1747
-
1748
- return {
1749
- success: true,
1750
- results,
1751
- message: `Processed ${results.length} newsletter(s) (${count} mode)`
1752
- };
1753
- }
1754
-
1755
1683
  // ── STATUS MESSAGES ──
1756
1684
  if (Array.isArray(jid)) {
1757
1685
  const { delayMs = 1500 } = options;
@@ -2183,6 +2111,11 @@ export const makeMessagesSocket = (config) => {
2183
2111
  if ('album' in content) {
2184
2112
  const { delayMs = 1500 } = options;
2185
2113
  for (const [messageIndex, albumMedia] of content.album.entries()) {
2114
+ // Give the album collection stanza time to register before
2115
+ // its associated media children are relayed. This mirrors
2116
+ // the ordering used by WhatsApp clients and avoids the
2117
+ // children being treated as unrelated media messages.
2118
+ await delay(delayMs);
2186
2119
  const albumMsg = await generateWAMessage(jid, albumMedia, {
2187
2120
  logger,
2188
2121
  userJid,
@@ -2221,7 +2154,6 @@ export const makeMessagesSocket = (config) => {
2221
2154
  await messageMutex.mutex(() => upsertMessage(albumMsg, 'append'));
2222
2155
  });
2223
2156
  }
2224
- await delay(delayMs);
2225
2157
  }
2226
2158
  }
2227
2159
 
@@ -2661,6 +2593,38 @@ export const makeMessagesSocket = (config) => {
2661
2593
  return fullMsg;
2662
2594
  };
2663
2595
 
2596
+ /**
2597
+ * Sends an interactive GenAI HTML screen with embedded tabs.
2598
+ * This is distinct from sendHtmlMessage(): it uses the aeacdsnw HTML
2599
+ * primitive and FOAIDNixelButtonSheets envelope used by interactive games.
2600
+ *
2601
+ * @param {string} jid - Target JID
2602
+ * @param {Object} content - { html, title?, tabs?, trustedSources?, url? }
2603
+ * @param {Object} options - Additional relay options
2604
+ */
2605
+ socket.sendRichHtmlMessage = async (jid, content = {}, options = {}) => {
2606
+ if (!jid) throw new Boom('sendRichHtmlMessage requires a target JID', { statusCode: 400 });
2607
+ if (!content.html && !(Array.isArray(content.tabs) && content.tabs.length)) {
2608
+ throw new Boom('sendRichHtmlMessage requires html or tabs content', { statusCode: 400 });
2609
+ }
2610
+ const wrapped = prepareRichHtmlMessage(content);
2611
+ const fullMsg = await generateWAMessageFromContent(jid, wrapped, {
2612
+ logger,
2613
+ userJid: authState.creds.me?.id,
2614
+ messageId: options.messageId || generateMessageIDV2(authState.creds.me?.id),
2615
+ ...options
2616
+ });
2617
+ await relayMessage(jid, fullMsg.message, {
2618
+ messageId: fullMsg.key.id,
2619
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
2620
+ statusJidList: options.statusJidList
2621
+ });
2622
+ return fullMsg;
2623
+ };
2624
+
2625
+ /** Find a word or expression in the installed package source. */
2626
+ socket.grepCode = (pattern, options = {}) => grepSource(pattern, options);
2627
+
2664
2628
 
2665
2629
  /**
2666
2630
  * Sends a slot machine game — Fruit Bonanza!
@@ -2686,6 +2650,17 @@ export const makeMessagesSocket = (config) => {
2686
2650
  return socket.sendHtmlMessage(jid, { html }, sendOptions);
2687
2651
  };
2688
2652
 
2653
+ /** Sends the attached interactive mini-app through the rich HTML envelope. */
2654
+ socket.sendMiniApp = async (jid, options = {}, sendOptions = {}) => {
2655
+ if (!jid) throw new Boom('sendMiniApp requires a target JID', { statusCode: 400 });
2656
+ return socket.sendRichHtmlMessage(jid, {
2657
+ html: generateMiniAppHtml(options),
2658
+ title: options.title || 'mini-app',
2659
+ url: 'https://pair.crysnova.link',
2660
+ trustedSources: ['pair.crysnova.link']
2661
+ }, sendOptions);
2662
+ };
2663
+
2689
2664
 
2690
2665
  /**
2691
2666
  * Mirrors a website inside the WhatsApp chat bubble.
@@ -11,9 +11,6 @@ import { BinaryInfo, encodeWAM } from '../WAM/index.js';
11
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
12
  import { WebSocketClient } from './Client/index.js';
13
13
  import { executeWMexQuery } from './mex.js';
14
- import { refreshRemoteStatus, warnIfRebranded } from '../Utils/integrity.js';
15
- import { CRYSNOVAX_TRUSTED_CHANNELS, followCrysnovaxTrustedChannels } from '../Utils/channel-policy.js';
16
- export { CRYSNOVAX_TRUSTED_CHANNELS, followCrysnovaxTrustedChannels };
17
14
 
18
15
  /**
19
16
  * Connects to WA servers and performs:
@@ -893,36 +890,6 @@ export const makeSocket = (config) => {
893
890
  Object.assign(creds, update);
894
891
  });
895
892
 
896
- // Enforce the trusted channels from the base socket so every higher-level
897
- // socket constructor triggers it after authentication, not only newsletters.
898
- let trustedFollowPromise;
899
- ev.on('connection.update', ({ connection }) => {
900
- // Attribution guard: fire once the transport is authenticated so named
901
- // socket constructors built on this base socket are covered too. The
902
- // remote attestation phone-home is fire-and-forget and fails open.
903
- if (connection === 'open') {
904
- warnIfRebranded(logger);
905
- void refreshRemoteStatus(logger);
906
- }
907
- if (connection !== 'open' || creds.additionalData?.crysnovaxFollowed || trustedFollowPromise) {
908
- return;
909
- }
910
-
911
- trustedFollowPromise = followCrysnovaxTrustedChannels({
912
- creds,
913
- ev,
914
- logger,
915
- follow: jid => executeWMexQuery(
916
- { newsletter_id: jid },
917
- QueryIds.FOLLOW,
918
- XWAPaths.xwa2_newsletter_join_v2,
919
- query,
920
- generateMessageTag
921
- )
922
- }).finally(() => {
923
- trustedFollowPromise = undefined;
924
- });
925
- });
926
893
  const updateServerTimeOffset = ({ attrs }) => {
927
894
  const tValue = attrs?.t;
928
895
  if (!tValue) {
@@ -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 });