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 +113 -1
- package/engine-requirements.js +0 -36
- package/lib/Socket/chats.js +73 -0
- package/lib/Socket/index.js +0 -34
- package/lib/Socket/messages-send.js +85 -110
- package/lib/Socket/socket.js +0 -33
- package/lib/Utils/games/miniapp.js +5 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/message-composer.js +2 -2
- package/lib/Utils/messages.js +145 -76
- package/lib/Utils/meta-compositing.js +0 -11
- package/lib/Utils/rich-message-utils.js +59 -0
- package/lib/Utils/source-grep.js +45 -0
- package/lib/Utils/welcome-flow.js +2 -6
- package/package.json +1 -1
- package/lib/Socket/messages-send.js[latest working] +0 -1759
- package/lib/Utils/integrity.js +0 -296
package/README.md
CHANGED
|
@@ -10,7 +10,16 @@
|
|
|
10
10
|
npm install plogme
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The current package version is **
|
|
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
|
package/engine-requirements.js
CHANGED
|
@@ -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
|
-
}
|
package/lib/Socket/chats.js
CHANGED
|
@@ -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
|
*/
|
package/lib/Socket/index.js
CHANGED
|
@@ -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
|
-
|
|
1298
|
-
|
|
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.
|
package/lib/Socket/socket.js
CHANGED
|
@@ -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
|
+
};
|
package/lib/Utils/index.js
CHANGED
|
@@ -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 });
|