plogme 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { proto } from '../../WAProto/index.js';
2
2
  import { makeLibSignalRepository } from '../Signal/libsignal.js';
3
3
  import { Browsers } from '../Utils/browser-utils.js';
4
4
  import logger from '../Utils/logger.js';
5
- const version = [2, 3000, 1047412487];
5
+ const version = [2, 3000, 1049983640];
6
6
  export const UNAUTHORIZED_CODES = [401, 403, 419];
7
7
  export const BIZ_BOT_SUPPORT_PAYLOAD = '{"version":1,"is_ai_message":true,"should_upload_client_logs":false,"should_show_system_message":false,"ticket_id":"7004947587700716","citation_items":[],"ticket_locale":"us"}';
8
8
  export const DEFAULT_ORIGIN = 'https://web.whatsapp.com';
@@ -1,11 +1,11 @@
1
1
  import NodeCache from '@cacheable/node-cache';
2
2
  import { Boom } from '@hapi/boom';
3
- import { randomBytes } from 'crypto';
3
+ import { randomBytes, randomUUID } from 'crypto';
4
4
  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, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { getUrlInfo } from '../Utils/link-preview.js';
8
- import { prepareRichMenuMessage, prepareA2UIMessage, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareSocialEntityMessage, prepareInstagramProfileMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareSocialProfileMessage, prepareUnifiedResponseMessage, prepareRichMapMessage, prepareBotPromptSuggestionsMessage, prepareBotSourcesMessage, prepareRichDynamicMediaMessage, prepareBotPttTranscriptMessage, prepareGridImageMessage, prepareVerticalAlbumMessage, prepareVideoGridMessage, prepareHtmlMessage, prepareRichHtmlMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
8
+ import { prepareRichMenuMessage, prepareA2UIMessage, prepareA2UICommandMenu, prepareAIRichCommandMenu, prepareSocialEntityMessage, prepareInstagramProfileMessage, prepareRichResponseMessage, prepareRichGenerationMessage, prepareCopyToClipboardMessage, prepareRichTextMessage, prepareRichQuoteMessage, prepareRichImageMessage, prepareRichLinkMessage, prepareSocialProfileMessage, prepareUnifiedResponseMessage, prepareRichMapMessage, prepareBotPromptSuggestionsMessage, prepareBotSourcesMessage, prepareRichDynamicMediaMessage, prepareBotPttTranscriptMessage, prepareGridImageMessage, prepareVerticalAlbumMessage, prepareVideoGridMessage, prepareHtmlMessage, prepareRichHtmlMessage, buildRichLabelMessage, attachBotVerificationProofs, hasVerifiableProofs } from '../Utils/rich-message-utils.js';
9
9
  import { generateSlotMachineHtml } from '../Utils/games/slot-machine.js';
10
10
  import { generateMiniAppHtml } from '../Utils/games/miniapp.js';
11
11
  import { generateWebsitePreviewHtml, generateLinkPreviewHtml } from '../Utils/games/website-preview.js';
@@ -233,6 +233,16 @@ export const makeMessagesSocket = (config) => {
233
233
  const decoded = jidDecode(jid);
234
234
  const user = decoded?.user;
235
235
  const device = decoded?.device;
236
+ // Meta AI/bot JIDs are single device-0 endpoints. USync does not
237
+ // enumerate them, so inject the endpoint instead of dropping it.
238
+ if (user && (isJidMetaAI(jid) || isJidBot(jid))) {
239
+ deviceResults.push({
240
+ user,
241
+ device: typeof device === 'number' ? device : 0,
242
+ jid
243
+ });
244
+ return null;
245
+ }
236
246
  const isExplicitDevice = typeof device === 'number' && device >= 0;
237
247
  if (isExplicitDevice && user) {
238
248
  deviceResults.push({
@@ -396,7 +406,10 @@ export const makeMessagesSocket = (config) => {
396
406
  if (jidsRequiringFetch.length) {
397
407
  const wireJids = [
398
408
  ...jidsRequiringFetch.filter(jid => !!isLidUser(jid) || !!isHostedLidUser(jid)),
399
- ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => !!isPnUser(jid) || !!isHostedPnUser(jid)))) || []).map(a => a.lid)
409
+ ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => !!isPnUser(jid) || !!isHostedPnUser(jid)))) || []).map(a => a.lid),
410
+ // @bot endpoints are not represented by LID/PN USync results,
411
+ // but still require a pre-key fetch to build the Signal session.
412
+ ...jidsRequiringFetch.filter(jid => isJidMetaAI(jid) || isJidBot(jid))
400
413
  ];
401
414
  logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions');
402
415
  const result = await query({
@@ -677,6 +690,16 @@ export const makeMessagesSocket = (config) => {
677
690
  if (isStatus && statusJidList) {
678
691
  participantsList.push(...statusJidList);
679
692
  }
693
+ // Bots invoked through botGroupMetadata are not group members,
694
+ // so include their @bot endpoints in sender-key distribution.
695
+ const botGroupParticipants = message?.messageContextInfo?.botMetadata?.botGroupMetadata?.participantsMetadata || [];
696
+ for (const participant of botGroupParticipants) {
697
+ if (!participant?.botFbid)
698
+ continue;
699
+ const botJid = `${participant.botFbid}@bot`;
700
+ if (!participantsList.includes(botJid))
701
+ participantsList.push(botJid);
702
+ }
680
703
  const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false);
681
704
  devices.push(...additionalDevices);
682
705
  if (isGroup) {
@@ -702,7 +725,8 @@ export const makeMessagesSocket = (config) => {
702
725
  for (const device of devices) {
703
726
  const deviceJid = device.jid;
704
727
  const hasKey = !!senderKeyMap[deviceJid];
705
- if ((!hasKey || !!participant) &&
728
+ const isBotRecipient = isJidMetaAI(deviceJid) || isJidBot(deviceJid);
729
+ if ((!hasKey || !!participant || isBotRecipient) &&
706
730
  !isHostedLidUser(deviceJid) &&
707
731
  !isHostedPnUser(deviceJid) &&
708
732
  device.device !== 99) {
@@ -1301,7 +1325,7 @@ export const makeMessagesSocket = (config) => {
1301
1325
  },
1302
1326
  sendCarousel: async (jid, cards, options = {}) => {
1303
1327
  if (!Array.isArray(cards) || cards.length === 0) {
1304
- throw new Boom('sendCarousel requires a non-empty cards array', { statusCode: 400 });
1328
+ throw new Boom('sendCarousel requires a non-empty card array', { statusCode: 400 });
1305
1329
  }
1306
1330
  const normalizedCards = cards.map(card => ({
1307
1331
  ...card,
@@ -1317,6 +1341,7 @@ export const makeMessagesSocket = (config) => {
1317
1341
  const fullMsg = await generateWAMessage(jid, {
1318
1342
  ...messageOptions,
1319
1343
  cards: normalizedCards,
1344
+ ...(options.carouselCardType != null ? { carouselCardType: options.carouselCardType } : {}),
1320
1345
  ...(text !== undefined ? { text } : {}),
1321
1346
  ...(footer !== undefined ? { footer } : {})
1322
1347
  }, {
@@ -1379,76 +1404,71 @@ export const makeMessagesSocket = (config) => {
1379
1404
  return fullMsg;
1380
1405
  },
1381
1406
  sendMetaAI: async (a, b, c = {}) => {
1382
- // baron signature: (jid, text, opts) — README signature: (text, opts)
1383
- let text;
1384
- let opts = c;
1385
- if (typeof b === 'string') {
1386
- text = b;
1387
- }
1388
- else {
1389
- text = a;
1390
- opts = b || {};
1391
- }
1392
- const yourJid = typeof b === 'string' ? a : (opts.yourJid || '');
1407
+ // Supports both (text, opts) and the historical (jid, text, opts)
1408
+ // call shape. The payload below mirrors the verified WA-Web Hatch send.
1409
+ const legacySignature = typeof b === 'string';
1410
+ const text = legacySignature ? b : a;
1411
+ const opts = legacySignature ? c : (b || {});
1393
1412
  const META_AI_BOT_JID = '867051314767696@bot';
1413
+ const META_AI_FBID = '867051314767696';
1394
1414
  const jid = opts.jid || META_AI_BOT_JID;
1395
- const threadId = opts.threadId || generateMessageIDV2(yourJid);
1396
- const now = Date.now();
1397
- const senderTimestamp = opts.senderTimestamp || String(Math.floor(now / 1000));
1415
+ const isGroup = jid.endsWith('@g.us');
1416
+ const invokerJid = opts.invokerJid || authState.creds.me?.id;
1398
1417
  const messageSecret = opts.messageSecret || randomBytes(32);
1399
- const senderKeyHash = opts.senderKeyHash || randomBytes(8).toString('base64');
1400
- const message = {
1401
- extendedTextMessage: proto.Message.ExtendedTextMessage.fromObject({
1402
- text,
1403
- previewType: 'NONE',
1404
- contextInfo: proto.ContextInfo.fromObject({
1405
- botMessageSharingInfo: {
1406
- botEntryPointOrigin: 'FAVICON',
1407
- forwardScore: 0
1408
- }
1409
- }),
1410
- inviteLinkGroupTypeV2: 'DEFAULT'
1411
- }),
1412
- messageContextInfo: proto.MessageContextInfo.fromObject({
1413
- deviceListMetadata: {
1414
- senderKeyHash,
1415
- senderTimestamp
1418
+ const rendering = {
1419
+ bloksVersioningId: 'd7c89519fd412464e4a4ddd60b2e5fa57c5000766a697d60835246ef0952daba',
1420
+ pixelDensity: 2.625
1421
+ };
1422
+ const botMetadata = isGroup
1423
+ ? {
1424
+ botGroupMetadata: {
1425
+ participantsMetadata: [{ botFbid: opts.botFbid || META_AI_FBID }]
1416
1426
  },
1417
- deviceListMetadataVersion: 2,
1418
- messageSecret,
1419
- botMetadata: {
1420
- botModeSelectionMetadata: {
1421
- overrideMode: [0]
1422
- },
1423
- botThreadInfo: {
1424
- serverInfo: { title: text.substring(0, 50) },
1425
- clientInfo: { type: 'DEFAULT' }
1426
- },
1427
- botRenderingConfigMetadata: {
1428
- bloksVersioningId: '1eb86e6f4117d052e6bab62fe758a2e2af43747b85c5c1a886c8262bac462ea4',
1429
- pixelDensity: 2.625
1430
- },
1431
- ...(opts.conversationContext?.length ? { aiConversationContext: opts.conversationContext } : {})
1427
+ botRenderingConfigMetadata: rendering
1428
+ }
1429
+ : {
1430
+ personaId: jid,
1431
+ invokerJid,
1432
+ sessionMetadata: {
1433
+ sessionId: opts.sessionId || randomUUID(),
1434
+ sessionSource: 3
1432
1435
  },
1433
- threadId: [
1434
- {
1435
- threadType: 'AI_THREAD',
1436
- threadKey: {
1437
- remoteJid: '0002@s.whatsapp.net',
1438
- fromMe: true,
1439
- id: threadId
1440
- }
1441
- }
1442
- ]
1443
- })
1436
+ capabilityMetadata: {
1437
+ capabilities: opts.capabilities || Array.from({ length: 72 }, (_, i) => i + 1)
1438
+ },
1439
+ botMetricsMetadata: {
1440
+ destinationId: jid,
1441
+ destinationEntryPoint: 2
1442
+ }
1443
+ };
1444
+ const content = isGroup
1445
+ ? (opts.mention
1446
+ ? { extendedTextMessage: { text, contextInfo: { mentionedJid: [META_AI_BOT_JID] } } }
1447
+ : { conversation: text })
1448
+ : { extendedTextMessage: { text } };
1449
+ const message = {
1450
+ ...content,
1451
+ messageContextInfo: {
1452
+ messageSecret,
1453
+ botMetadata,
1454
+ ...(opts.conversationContext?.length ? { aiConversationContext: opts.conversationContext } : {})
1455
+ }
1444
1456
  };
1445
- const msgId = generateMessageIDV2(yourJid);
1446
- const messageOptions = {
1457
+ const msgId = generateMessageIDV2(invokerJid || '');
1458
+ const relayOptions = {
1447
1459
  messageId: msgId,
1448
1460
  ...(opts.quoted ? { quoted: opts.quoted } : {}),
1449
- ...(opts.links ? { links: opts.links } : {})
1461
+ ...(opts.links ? { links: opts.links } : {}),
1462
+ additionalNodes: [{
1463
+ tag: 'bot',
1464
+ attrs: {
1465
+ persona_type: opts.personaType || 'default',
1466
+ agent_engagement_type: opts.agentEngagementType || (isGroup ? 'group_chat' : 'direct_chat'),
1467
+ mode_selected: opts.modeSelected || '0'
1468
+ }
1469
+ }]
1450
1470
  };
1451
- await relayMessage(jid, message, messageOptions);
1471
+ await relayMessage(jid, message, relayOptions);
1452
1472
  return msgId;
1453
1473
  },
1454
1474
  // Additive Nexus-compatible AI-rich helpers. Existing Gen4/table/grid/carousel APIs remain unchanged.
@@ -1588,6 +1608,19 @@ export const makeMessagesSocket = (config) => {
1588
1608
  }
1589
1609
  return fullMsg;
1590
1610
  },
1611
+ /**
1612
+ * Send a compact forwarded AIRich label bubble. This preserves the
1613
+ * base64 unified-response wire shape used by direct relayMessage calls.
1614
+ */
1615
+ sendRichLabelMessage: async (jid, content = {}, options = {}) => {
1616
+ if (!jid) throw new Boom('sendRichLabelMessage requires a target JID', { statusCode: 400 });
1617
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
1618
+ throw new Boom('sendRichLabelMessage content must be an object', { statusCode: 400 });
1619
+ }
1620
+ const message = buildRichLabelMessage(content);
1621
+ await relayMessage(jid, message, options);
1622
+ return message;
1623
+ },
1591
1624
  captureAndResendUnifiedResponse: async (jid, metaAiMsg, quoted, options = {}) => {
1592
1625
  const captured = captureUnifiedResponse(metaAiMsg);
1593
1626
  if (!captured) {
@@ -8,10 +8,11 @@ export const ButtonType = proto.Message.ButtonsMessage.Button.Type;
8
8
  export const CarouselCardType = proto.Message.InteractiveMessage.CarouselMessage.CarouselCardType;
9
9
  export const ListType = proto.Message.ListMessage.ListType;
10
10
  export const ProtocolType = proto.Message.ProtocolMessage.Type;
11
+ export const EventInviteStatus = proto.Message.EventInviteMessage.EventStatus;
11
12
  export const WAMessageStubType = proto.WebMessageInfo.StubType;
12
13
  export const WAMessageStatus = proto.WebMessageInfo.Status;
13
14
  export var WAMessageAddressingMode;
14
15
  (function (WAMessageAddressingMode) {
15
16
  WAMessageAddressingMode["PN"] = "pn";
16
17
  WAMessageAddressingMode["LID"] = "lid";
17
- })(WAMessageAddressingMode || (WAMessageAddressingMode = {}));
18
+ })(WAMessageAddressingMode || (WAMessageAddressingMode = {}));
@@ -11,8 +11,7 @@
11
11
  * → edit: step[0] → DONE
12
12
  * → edit: step[1] → DONE
13
13
  * → ...
14
- * → delete placeholder
15
- * → send final message fresh
14
+ * → MESSAGE_EDIT: final answer on the same message key
16
15
  *
17
16
  * If you use or copy this code, please credit @crysnovax/bailey.
18
17
  */
@@ -119,19 +118,17 @@ export const replayPlanning = async (sock, jid, steps, finalContent, {
119
118
  await delay(finalPauseMs);
120
119
  }
121
120
 
122
- if (key && !aborted) {
123
- await sock.sendMessage(jid, { delete: key });
124
- }
125
121
  } catch (err) {
126
- try { if (key) await sock.sendMessage(jid, { delete: key }); } catch (_) {}
122
+ // Keep the latest successfully rendered frame if an intermediate edit fails.
127
123
  }
128
124
 
129
125
  await sock.sendPresenceUpdate('paused', jid);
130
126
 
131
- // Skip final send if _skipFinalSend is set (for replayPlanningOnly)
132
- if (sendOptions._skipFinalSend) return placeholder;
127
+ // Skip the final edit if _skipFinalSend is set (for replayPlanningOnly).
128
+ if (sendOptions._skipFinalSend || !key || aborted) return placeholder;
133
129
 
134
- return sock.sendMessage(jid, finalContent, sendOptions);
130
+ const { _skipFinalSend: _ignored, ...finalSendOptions } = sendOptions;
131
+ return sock.sendMessage(jid, { ...finalContent, edit: key }, finalSendOptions);
135
132
  };
136
133
 
137
134
  /**
@@ -832,6 +832,14 @@ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) =
832
832
  else if (action?.lockChatAction) {
833
833
  ev.emit('chats.lock', { id: id, locked: !!action.lockChatAction.locked });
834
834
  }
835
+ else if (action?.communityNestingStateAction) {
836
+ const nestingState = action.communityNestingStateAction.nestingState;
837
+ ev.emit('groups.update', [{
838
+ id,
839
+ nestingState,
840
+ nested: nestingState === proto.SyncActionValue.CommunityNestingStateAction.NestingState.NESTING_STATE_NESTED
841
+ }]);
842
+ }
835
843
  else if (action?.privacySettingDisableLinkPreviewsAction) {
836
844
  ev.emit('settings.update', {
837
845
  setting: 'disableLinkPreviews',
@@ -39,6 +39,8 @@ export const processHistoryMessage = (item, logger) => {
39
39
  const contacts = [];
40
40
  const chats = [];
41
41
  const lidPnMappings = [];
42
+ const favoriteStickers = [];
43
+ const omittedConversations = [];
42
44
  logger?.trace({ progress: item.progress }, 'processing history of type ' + item.syncType?.toString());
43
45
  // Extract LID-PN mappings for all sync types
44
46
  for (const m of item.phoneNumberToLidMappings || []) {
@@ -46,6 +48,21 @@ export const processHistoryMessage = (item, logger) => {
46
48
  lidPnMappings.push({ lid: m.lidJid, pn: m.pnJid });
47
49
  }
48
50
  }
51
+ const favoriteStickerKeys = new Set();
52
+ for (const sticker of item.favoriteStickers || []) {
53
+ const key = sticker.fileSha256 ? Buffer.from(sticker.fileSha256).toString('base64') : undefined;
54
+ if (!key || !favoriteStickerKeys.has(key)) {
55
+ if (key)
56
+ favoriteStickerKeys.add(key);
57
+ favoriteStickers.push(sticker);
58
+ }
59
+ }
60
+ for (const omitted of item.omittedConversations || []) {
61
+ omittedConversations.push({
62
+ id: omitted.historyChatId,
63
+ accountLid: omitted.accountLid || undefined
64
+ });
65
+ }
49
66
  switch (item.syncType) {
50
67
  case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP:
51
68
  case proto.HistorySync.HistorySyncType.RECENT:
@@ -109,6 +126,9 @@ export const processHistoryMessage = (item, logger) => {
109
126
  contacts,
110
127
  messages,
111
128
  lidPnMappings,
129
+ favoriteStickers,
130
+ omittedConversations,
131
+ omittedConversationsProvided: item.omittedConversationsProvided ?? false,
112
132
  syncType: item.syncType,
113
133
  progress: item.progress
114
134
  };
@@ -127,4 +147,4 @@ export const getHistoryMsg = (message) => {
127
147
  const normalizedContent = !!message ? normalizeMessageContent(message) : undefined;
128
148
  const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification;
129
149
  return anyHistoryMsg;
130
- };
150
+ };
@@ -835,6 +835,33 @@ export const generateWAMessageContent = async (message, options) => {
835
835
  else if (hasNonNullishProperty(message, 'product')) {
836
836
  m.productMessage = await prepareProductMessage(message, options);
837
837
  }
838
+ else if (hasNonNullishProperty(message, 'eventInvite') || hasNonNullishProperty(message, 'eventInviteMessage')) {
839
+ const invite = message.eventInvite || message.eventInviteMessage;
840
+ if (!invite.eventId && !invite.id) {
841
+ throw new Boom('eventInvite requires an eventId', { statusCode: 400 });
842
+ }
843
+ const startTime = invite.startDate instanceof Date
844
+ ? Math.floor(invite.startDate.getTime() / 1000)
845
+ : invite.startTime != null ? Number(invite.startTime) : undefined;
846
+ const endTime = invite.endDate instanceof Date
847
+ ? Math.floor(invite.endDate.getTime() / 1000)
848
+ : invite.endTime != null ? Number(invite.endTime) : undefined;
849
+ m.eventInviteMessage = proto.Message.EventInviteMessage.fromObject({
850
+ eventId: invite.eventId || invite.id,
851
+ eventTitle: invite.title || invite.eventTitle || '',
852
+ caption: invite.text || invite.caption || '',
853
+ startTime,
854
+ endTime,
855
+ isCanceled: invite.isCancelled ?? invite.isCanceled ?? false,
856
+ jpegThumbnail: invite.thumbnail || invite.jpegThumbnail,
857
+ callLink: invite.callLink,
858
+ coverImageHandle: invite.coverImageHandle,
859
+ locationName: invite.locationName,
860
+ lastUpdatedTsUsec: invite.lastUpdatedTsUsec,
861
+ status: invite.status,
862
+ contextInfo: invite.contextInfo
863
+ });
864
+ }
838
865
  else if (hasNonNullishProperty(message, 'event')) {
839
866
  m.eventMessage = {};
840
867
  const startTime = Math.floor(message.event.startDate.getTime() / 1000);
@@ -1072,6 +1099,18 @@ export const generateWAMessageContent = async (message, options) => {
1072
1099
  else if (hasNonNullishProperty(message, 'requestPhoneNumber')) {
1073
1100
  m.requestPhoneNumberMessage = {};
1074
1101
  }
1102
+ else if (hasNonNullishProperty(message, 'requestLocation')) {
1103
+ m.requestLocationMessage = proto.Message.RequestLocationMessage.fromObject({});
1104
+ }
1105
+ else if (hasNonNullishProperty(message, 'requestLocationUpdate')) {
1106
+ const request = message.requestLocationUpdate || {};
1107
+ const updateType = proto.Message.RequestLocationUpdateMessage.UpdateType;
1108
+ m.requestLocationUpdateMessage = proto.Message.RequestLocationUpdateMessage.fromObject({
1109
+ key: request.key,
1110
+ updateType: request.cancel ? updateType.CANCEL : (request.updateType ?? updateType.UNKNOWN),
1111
+ senderTimestampMs: request.senderTimestampMs ?? Date.now()
1112
+ });
1113
+ }
1075
1114
  else if (hasNonNullishProperty(message, 'chatTheme')) {
1076
1115
  const theme = message.chatTheme || {};
1077
1116
  const themeSetting = {};
@@ -1080,6 +1119,31 @@ export const generateWAMessageContent = async (message, options) => {
1080
1119
  (String(theme.chatThemeId).includes('@') ? theme.chatThemeId : `${theme.chatThemeId}@Tonal`);
1081
1120
  themeSetting.colorSchemeId = String(colorSchemeId);
1082
1121
  }
1122
+ if (theme.solidColor) {
1123
+ const solidColor = typeof theme.solidColor === 'object' ? theme.solidColor : {};
1124
+ const hexToArgb = (value, fallback) => {
1125
+ if (typeof value === 'number')
1126
+ return value >>> 0;
1127
+ if (typeof value !== 'string')
1128
+ return fallback;
1129
+ const hex = value.replace('#', '');
1130
+ if (hex.length === 6)
1131
+ return (0xff000000 | parseInt(hex, 16)) >>> 0;
1132
+ if (hex.length === 8)
1133
+ return parseInt(hex, 16) >>> 0;
1134
+ return fallback;
1135
+ };
1136
+ const solid = {
1137
+ backgroundLightArgb: hexToArgb(solidColor.colorLight || solidColor.color, 0xffffffff),
1138
+ backgroundDarkArgb: hexToArgb(solidColor.colorDark || solidColor.color, 0xff000000),
1139
+ isDoodleEnabled: solidColor.isDoodleEnabled ?? false
1140
+ };
1141
+ if (solidColor.doodleColorLight != null)
1142
+ solid.doodleLightArgb = hexToArgb(solidColor.doodleColorLight);
1143
+ if (solidColor.doodleColorDark != null)
1144
+ solid.doodleDarkArgb = hexToArgb(solidColor.doodleColorDark);
1145
+ themeSetting.solidColor = proto.Message.ChatSolidColorWallpaper.fromObject(solid);
1146
+ }
1083
1147
  if (theme.animatedWallpaper) {
1084
1148
  const wallpaper = theme.animatedWallpaper;
1085
1149
  themeSetting.animatedWallpaper = proto.Message.ChatAnimatedWallpaper.fromObject({
@@ -1115,6 +1179,13 @@ export const generateWAMessageContent = async (message, options) => {
1115
1179
  }
1116
1180
  };
1117
1181
  }
1182
+ else if (hasNonNullishProperty(message, 'cards')) {
1183
+ // Carousel composition is handled by the interactive-message chain
1184
+ // below. Mark this shape as recognized so cards-only sends do not
1185
+ // fall through to prepareWAMessageMedia(message), which has no
1186
+ // top-level media key and incorrectly throws Invalid media type.
1187
+ m = {};
1188
+ }
1118
1189
  else {
1119
1190
  m = await prepareWAMessageMedia(message, options);
1120
1191
  if (m.audioMessage && options.backgroundColor !== undefined) {
@@ -1301,6 +1372,9 @@ export const generateWAMessageContent = async (message, options) => {
1301
1372
  m = { interactiveMessage };
1302
1373
  }
1303
1374
  else if (hasNonNullishProperty(message, 'cards')) {
1375
+ const carouselCardType = typeof message.carouselCardType === 'string'
1376
+ ? ({ horizontal: CarouselCardType.HSCROLL_CARDS, hscroll: CarouselCardType.HSCROLL_CARDS, album: CarouselCardType.ALBUM_IMAGE }[message.carouselCardType.toLowerCase()] ?? CarouselCardType.HSCROLL_CARDS)
1377
+ : (message.carouselCardType ?? CarouselCardType.HSCROLL_CARDS);
1304
1378
  const interactiveMessage = {
1305
1379
  carouselMessage: {
1306
1380
  cards: await Promise.all(message.cards.map(async (card) => {
@@ -1326,18 +1400,17 @@ export const generateWAMessageContent = async (message, options) => {
1326
1400
  hasMediaAttachment: isValidHeader
1327
1401
  }
1328
1402
  };
1329
- if (hasOptionalProperty(card, 'text')) {
1330
- carouselCard.body = { text: card.text };
1403
+ const cardBodyText = card.text ?? card.body?.text ?? card.caption;
1404
+ if (cardBodyText != null) {
1405
+ carouselCard.body = { text: cardBodyText };
1331
1406
  }
1332
- else {
1333
- if (hasOptionalProperty(card, 'caption')) {
1334
- carouselCard.body = { text: card.caption };
1335
- }
1336
- if (hasOptionalProperty(card, 'thumbnail') && !!card.thumbnail) {
1337
- carouselCard.jpegThumbnail = card.thumbnail;
1338
- }
1339
- Object.assign(carouselCard.header, carouselHeader);
1407
+ if (hasOptionalProperty(card, 'thumbnail') && !!card.thumbnail) {
1408
+ carouselCard.jpegThumbnail = card.thumbnail;
1340
1409
  }
1410
+ // The media header must be attached even when the card has
1411
+ // a body text. The old else-branch dropped image/video
1412
+ // headers for text-bearing cards, producing blank cards.
1413
+ Object.assign(carouselCard.header, carouselHeader);
1341
1414
  if (hasOptionalProperty(card, 'audioFooter')) {
1342
1415
  const { audioMessage } = await prepareWAMessageMedia({
1343
1416
  audio: card.audioFooter
@@ -1352,7 +1425,7 @@ export const generateWAMessageContent = async (message, options) => {
1352
1425
  }
1353
1426
  return carouselCard;
1354
1427
  })),
1355
- carouselCardType: CarouselCardType.UNKNOWN,
1428
+ carouselCardType,
1356
1429
  messageVersion: 1
1357
1430
  }
1358
1431
  };
@@ -65,10 +65,10 @@ const decodeDecryptedMsmsgMessage = (decrypted) => {
65
65
  const unpadded = Buffer.from(unpadRandomMax16(buf));
66
66
  const decoded = proto.Message.decode(unpadded);
67
67
  const hasContent = Object.keys(decoded).some(k => k !== 'messageContextInfo' && decoded[k] != null);
68
- if (hasContent) return decoded;
68
+ if (hasContent) return decodeUnifiedResponseInPlace(decoded);
69
69
  }
70
70
  catch {}
71
- return proto.Message.decode(buf);
71
+ return decodeUnifiedResponseInPlace(proto.Message.decode(buf));
72
72
  };
73
73
 
74
74
  /**
@@ -127,4 +127,77 @@ export const decryptMsmsgBotMessage = async (messageSecret, messageKey, msMsg) =
127
127
  throw err;
128
128
  };
129
129
 
130
+ // unifiedResponse.data is base64/bytes-wrapped JSON, not encrypted. Keep the
131
+ // original data and expose both the decoded object and flattened text.
132
+ export const parseUnifiedResponseData = (data) => {
133
+ if (!data)
134
+ return null;
135
+ try {
136
+ let buf;
137
+ if (Buffer.isBuffer(data))
138
+ buf = data;
139
+ else if (data instanceof Uint8Array)
140
+ buf = Buffer.from(data);
141
+ else if (typeof data === 'string')
142
+ buf = Buffer.from(data, 'base64');
143
+ else if (data.type === 'Buffer' && Array.isArray(data.data))
144
+ buf = Buffer.from(data.data);
145
+ else
146
+ return null;
147
+ return JSON.parse(buf.toString('utf8'));
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ };
153
+ export const textFromUnifiedResponse = (data) => {
154
+ const json = parseUnifiedResponseData(data);
155
+ if (!json)
156
+ return null;
157
+ const parts = [];
158
+ for (const section of json.sections || []) {
159
+ const text = section?.view_model?.primitive?.text;
160
+ if (text)
161
+ parts.push(text);
162
+ }
163
+ return parts.length ? parts.join('\n') : null;
164
+ };
165
+ export const textFromRichResponse = (richResponse) => {
166
+ if (!richResponse)
167
+ return null;
168
+ if (Array.isArray(richResponse.submessages)) {
169
+ const parts = richResponse.submessages
170
+ .map(submessage => submessage?.messageText)
171
+ .filter(text => typeof text === 'string' && text.length);
172
+ if (parts.length)
173
+ return parts.join('\n');
174
+ }
175
+ return textFromUnifiedResponse(richResponse.unifiedResponse?.data);
176
+ };
177
+ export const extractMetaAiText = (message) => {
178
+ if (!message)
179
+ return null;
180
+ const body = message.protocolMessage?.editedMessage || message;
181
+ return body.extendedTextMessage?.text ?? body.conversation ?? textFromRichResponse(body.richResponseMessage) ?? null;
182
+ };
183
+ export const decodeUnifiedResponseInPlace = (message) => {
184
+ if (!message || typeof message !== 'object')
185
+ return message;
186
+ const bodies = [
187
+ message,
188
+ message.protocolMessage?.editedMessage,
189
+ message.editedMessage
190
+ ].filter(Boolean);
191
+ for (const body of bodies) {
192
+ const unifiedResponse = body.richResponseMessage?.unifiedResponse;
193
+ if (unifiedResponse && unifiedResponse.data != null && unifiedResponse.decodedData === undefined) {
194
+ unifiedResponse.decodedData = parseUnifiedResponseData(unifiedResponse.data);
195
+ const text = textFromUnifiedResponse(unifiedResponse.data);
196
+ if (text != null)
197
+ unifiedResponse.text = text;
198
+ }
199
+ }
200
+ return message;
201
+ };
202
+
130
203
  export { decodeDecryptedMsmsgMessage };