@oxidezap/baileyrs 0.0.7
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/LICENSE +21 -0
- package/README.md +490 -0
- package/WAProto/index.d.ts +13875 -0
- package/WAProto/index.js +175 -0
- package/lib/Defaults/index.d.ts +9 -0
- package/lib/Defaults/index.d.ts.map +1 -0
- package/lib/Defaults/index.js +32 -0
- package/lib/Defaults/index.js.map +1 -0
- package/lib/Socket/blocking.d.ts +6 -0
- package/lib/Socket/blocking.d.ts.map +1 -0
- package/lib/Socket/blocking.js +9 -0
- package/lib/Socket/blocking.js.map +1 -0
- package/lib/Socket/chat-actions.d.ts +32 -0
- package/lib/Socket/chat-actions.d.ts.map +1 -0
- package/lib/Socket/chat-actions.js +78 -0
- package/lib/Socket/chat-actions.js.map +1 -0
- package/lib/Socket/contacts.d.ts +10 -0
- package/lib/Socket/contacts.d.ts.map +1 -0
- package/lib/Socket/contacts.js +21 -0
- package/lib/Socket/contacts.js.map +1 -0
- package/lib/Socket/events.d.ts +10 -0
- package/lib/Socket/events.d.ts.map +1 -0
- package/lib/Socket/events.js +293 -0
- package/lib/Socket/events.js.map +1 -0
- package/lib/Socket/groups.d.ts +28 -0
- package/lib/Socket/groups.d.ts.map +1 -0
- package/lib/Socket/groups.js +90 -0
- package/lib/Socket/groups.js.map +1 -0
- package/lib/Socket/index.d.ts +202 -0
- package/lib/Socket/index.d.ts.map +1 -0
- package/lib/Socket/index.js +378 -0
- package/lib/Socket/index.js.map +1 -0
- package/lib/Socket/messages.d.ts +42 -0
- package/lib/Socket/messages.d.ts.map +1 -0
- package/lib/Socket/messages.js +172 -0
- package/lib/Socket/messages.js.map +1 -0
- package/lib/Socket/newsletter.d.ts +9 -0
- package/lib/Socket/newsletter.d.ts.map +1 -0
- package/lib/Socket/newsletter.js +18 -0
- package/lib/Socket/newsletter.js.map +1 -0
- package/lib/Socket/presence.d.ts +7 -0
- package/lib/Socket/presence.d.ts.map +1 -0
- package/lib/Socket/presence.js +12 -0
- package/lib/Socket/presence.js.map +1 -0
- package/lib/Socket/profile.d.ts +14 -0
- package/lib/Socket/profile.d.ts.map +1 -0
- package/lib/Socket/profile.js +36 -0
- package/lib/Socket/profile.js.map +1 -0
- package/lib/Socket/transport.d.ts +12 -0
- package/lib/Socket/transport.d.ts.map +1 -0
- package/lib/Socket/transport.js +90 -0
- package/lib/Socket/transport.js.map +1 -0
- package/lib/Socket/types.d.ts +30 -0
- package/lib/Socket/types.d.ts.map +1 -0
- package/lib/Socket/types.js +3 -0
- package/lib/Socket/types.js.map +1 -0
- package/lib/Types/Auth.d.ts +144 -0
- package/lib/Types/Auth.d.ts.map +1 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Auth.js.map +1 -0
- package/lib/Types/BinaryNode.d.ts +15 -0
- package/lib/Types/BinaryNode.d.ts.map +1 -0
- package/lib/Types/BinaryNode.js +2 -0
- package/lib/Types/BinaryNode.js.map +1 -0
- package/lib/Types/Bussines.d.ts +25 -0
- package/lib/Types/Bussines.d.ts.map +1 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Bussines.js.map +1 -0
- package/lib/Types/Call.d.ts +15 -0
- package/lib/Types/Call.d.ts.map +1 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Call.js.map +1 -0
- package/lib/Types/Chat.d.ts +123 -0
- package/lib/Types/Chat.d.ts.map +1 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Chat.js.map +1 -0
- package/lib/Types/Contact.d.ts +24 -0
- package/lib/Types/Contact.d.ts.map +1 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Contact.js.map +1 -0
- package/lib/Types/Events.d.ts +238 -0
- package/lib/Types/Events.d.ts.map +1 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/Events.js.map +1 -0
- package/lib/Types/GroupMetadata.d.ts +67 -0
- package/lib/Types/GroupMetadata.d.ts.map +1 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/GroupMetadata.js.map +1 -0
- package/lib/Types/Label.d.ts +47 -0
- package/lib/Types/Label.d.ts.map +1 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/Label.js.map +1 -0
- package/lib/Types/LabelAssociation.d.ts +30 -0
- package/lib/Types/LabelAssociation.d.ts.map +1 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/LabelAssociation.js.map +1 -0
- package/lib/Types/Message.d.ts +298 -0
- package/lib/Types/Message.d.ts.map +1 -0
- package/lib/Types/Message.js +12 -0
- package/lib/Types/Message.js.map +1 -0
- package/lib/Types/Newsletter.d.ts +98 -0
- package/lib/Types/Newsletter.d.ts.map +1 -0
- package/lib/Types/Newsletter.js +31 -0
- package/lib/Types/Newsletter.js.map +1 -0
- package/lib/Types/Socket.d.ts +54 -0
- package/lib/Types/Socket.d.ts.map +1 -0
- package/lib/Types/Socket.js +2 -0
- package/lib/Types/Socket.js.map +1 -0
- package/lib/Types/State.d.ts +29 -0
- package/lib/Types/State.d.ts.map +1 -0
- package/lib/Types/State.js +2 -0
- package/lib/Types/State.js.map +1 -0
- package/lib/Types/index.d.ts +60 -0
- package/lib/Types/index.d.ts.map +1 -0
- package/lib/Types/index.js +25 -0
- package/lib/Types/index.js.map +1 -0
- package/lib/Utils/browser-utils.d.ts +3 -0
- package/lib/Utils/browser-utils.d.ts.map +1 -0
- package/lib/Utils/browser-utils.js +22 -0
- package/lib/Utils/browser-utils.js.map +1 -0
- package/lib/Utils/event-buffer.d.ts +7 -0
- package/lib/Utils/event-buffer.d.ts.map +1 -0
- package/lib/Utils/event-buffer.js +42 -0
- package/lib/Utils/event-buffer.js.map +1 -0
- package/lib/Utils/generics.d.ts +13 -0
- package/lib/Utils/generics.d.ts.map +1 -0
- package/lib/Utils/generics.js +40 -0
- package/lib/Utils/generics.js.map +1 -0
- package/lib/Utils/index.d.ts +10 -0
- package/lib/Utils/index.d.ts.map +1 -0
- package/lib/Utils/index.js +10 -0
- package/lib/Utils/index.js.map +1 -0
- package/lib/Utils/logger.d.ts +13 -0
- package/lib/Utils/logger.d.ts.map +1 -0
- package/lib/Utils/logger.js +3 -0
- package/lib/Utils/logger.js.map +1 -0
- package/lib/Utils/messages-media.d.ts +51 -0
- package/lib/Utils/messages-media.d.ts.map +1 -0
- package/lib/Utils/messages-media.js +223 -0
- package/lib/Utils/messages-media.js.map +1 -0
- package/lib/Utils/messages.d.ts +88 -0
- package/lib/Utils/messages.d.ts.map +1 -0
- package/lib/Utils/messages.js +730 -0
- package/lib/Utils/messages.js.map +1 -0
- package/lib/Utils/use-bridge-store.d.ts +13 -0
- package/lib/Utils/use-bridge-store.d.ts.map +1 -0
- package/lib/Utils/use-bridge-store.js +124 -0
- package/lib/Utils/use-bridge-store.js.map +1 -0
- package/lib/Utils/use-memory-store.d.ts +9 -0
- package/lib/Utils/use-memory-store.d.ts.map +1 -0
- package/lib/Utils/use-memory-store.js +23 -0
- package/lib/Utils/use-memory-store.js.map +1 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts +13 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -0
- package/lib/Utils/use-multi-file-auth-state.js +26 -0
- package/lib/Utils/use-multi-file-auth-state.js.map +1 -0
- package/lib/Utils/wrap-legacy-store.d.ts +36 -0
- package/lib/Utils/wrap-legacy-store.d.ts.map +1 -0
- package/lib/Utils/wrap-legacy-store.js +708 -0
- package/lib/Utils/wrap-legacy-store.js.map +1 -0
- package/lib/WABinary/index.d.ts +2 -0
- package/lib/WABinary/index.d.ts.map +1 -0
- package/lib/WABinary/index.js +2 -0
- package/lib/WABinary/index.js.map +1 -0
- package/lib/WABinary/jid-utils.d.ts +20 -0
- package/lib/WABinary/jid-utils.d.ts.map +1 -0
- package/lib/WABinary/jid-utils.js +51 -0
- package/lib/WABinary/jid-utils.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +9 -0
- package/lib/index.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { proto } from '../../WAProto/index.js';
|
|
5
|
+
import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
|
|
6
|
+
import { WAMessageStatus, WAProto } from '../Types/index.js';
|
|
7
|
+
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
8
|
+
import { unixTimestampSeconds } from './generics.js';
|
|
9
|
+
import { generateThumbnail, getAudioDuration, getAudioWaveform, getStream, toBuffer } from './messages-media.js';
|
|
10
|
+
const MIMETYPE_MAP = {
|
|
11
|
+
image: 'image/jpeg',
|
|
12
|
+
video: 'video/mp4',
|
|
13
|
+
document: 'application/pdf',
|
|
14
|
+
audio: 'audio/ogg; codecs=opus',
|
|
15
|
+
sticker: 'image/webp',
|
|
16
|
+
'product-catalog-image': 'image/jpeg'
|
|
17
|
+
};
|
|
18
|
+
const MessageTypeProto = {
|
|
19
|
+
image: WAProto.Message.ImageMessage,
|
|
20
|
+
video: WAProto.Message.VideoMessage,
|
|
21
|
+
audio: WAProto.Message.AudioMessage,
|
|
22
|
+
sticker: WAProto.Message.StickerMessage,
|
|
23
|
+
document: WAProto.Message.DocumentMessage
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Uses a regex to test whether the string contains a URL, and returns the URL if it does.
|
|
27
|
+
* @param text eg. hello https://google.com
|
|
28
|
+
* @returns the URL, eg. https://google.com
|
|
29
|
+
*/
|
|
30
|
+
export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
|
|
31
|
+
export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
|
|
32
|
+
if (!getUrlInfo)
|
|
33
|
+
return;
|
|
34
|
+
const url = extractUrlFromText(text);
|
|
35
|
+
if (url) {
|
|
36
|
+
try {
|
|
37
|
+
const urlInfo = await getUrlInfo(url);
|
|
38
|
+
return urlInfo;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// ignore if fails
|
|
42
|
+
logger?.warn({ trace: error.stack }, 'url generation failed');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const assertColor = (color) => {
|
|
47
|
+
if (typeof color === 'number') {
|
|
48
|
+
return color > 0 ? color : 0xffffffff + Number(color) + 1;
|
|
49
|
+
}
|
|
50
|
+
let hex = color.trim().replace('#', '');
|
|
51
|
+
if (hex.length <= 6) {
|
|
52
|
+
hex = 'FF' + hex.padStart(6, '0');
|
|
53
|
+
}
|
|
54
|
+
return parseInt(hex, 16);
|
|
55
|
+
};
|
|
56
|
+
export const prepareWAMessageMedia = async (message, options) => {
|
|
57
|
+
const logger = options.logger;
|
|
58
|
+
let mediaType;
|
|
59
|
+
for (const key of MEDIA_KEYS) {
|
|
60
|
+
if (key in message) {
|
|
61
|
+
mediaType = key;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!mediaType) {
|
|
65
|
+
throw new Boom('Invalid media type', { statusCode: 400 });
|
|
66
|
+
}
|
|
67
|
+
const messageRecord = message;
|
|
68
|
+
const uploadData = {
|
|
69
|
+
...message,
|
|
70
|
+
media: messageRecord[mediaType]
|
|
71
|
+
};
|
|
72
|
+
delete uploadData[mediaType];
|
|
73
|
+
// check if cacheable + generate cache key
|
|
74
|
+
const cacheableKey = typeof uploadData.media === 'object' &&
|
|
75
|
+
'url' in uploadData.media &&
|
|
76
|
+
!!uploadData.media.url &&
|
|
77
|
+
!!options.mediaCache &&
|
|
78
|
+
mediaType + ':' + uploadData.media.url.toString();
|
|
79
|
+
if (mediaType === 'document' && !uploadData.fileName) {
|
|
80
|
+
uploadData.fileName = 'file';
|
|
81
|
+
}
|
|
82
|
+
if (!uploadData.mimetype) {
|
|
83
|
+
uploadData.mimetype = MIMETYPE_MAP[mediaType];
|
|
84
|
+
}
|
|
85
|
+
if (cacheableKey) {
|
|
86
|
+
const mediaBuff = await options.mediaCache.get(cacheableKey);
|
|
87
|
+
if (mediaBuff) {
|
|
88
|
+
logger?.debug({ cacheableKey }, 'got media cache hit');
|
|
89
|
+
const obj = proto.Message.decode(mediaBuff);
|
|
90
|
+
const key = `${mediaType}Message`;
|
|
91
|
+
Object.assign(obj[key], { ...uploadData, media: undefined });
|
|
92
|
+
return obj;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Read media into buffer — needed for both upload and metadata extraction
|
|
96
|
+
const { stream } = await getStream(uploadData.media, options.options);
|
|
97
|
+
const buffer = await toBuffer(stream);
|
|
98
|
+
const effectiveMediaType = options.mediaTypeOverride || mediaType;
|
|
99
|
+
// Run upload + metadata extraction in parallel
|
|
100
|
+
let uploadResult;
|
|
101
|
+
if (options.processMedia) {
|
|
102
|
+
// User-provided processing pipeline (streaming encrypt, custom thumbnails, etc.)
|
|
103
|
+
const { upload, metadata } = await options.processMedia(buffer, effectiveMediaType, options.waClient);
|
|
104
|
+
uploadResult = upload;
|
|
105
|
+
if (metadata) {
|
|
106
|
+
if (metadata.jpegThumbnail !== undefined)
|
|
107
|
+
uploadData.jpegThumbnail = metadata.jpegThumbnail;
|
|
108
|
+
if (metadata.width !== undefined)
|
|
109
|
+
uploadData.width = metadata.width;
|
|
110
|
+
if (metadata.height !== undefined)
|
|
111
|
+
uploadData.height = metadata.height;
|
|
112
|
+
if (metadata.seconds !== undefined)
|
|
113
|
+
uploadData.seconds = metadata.seconds;
|
|
114
|
+
if (metadata.waveform !== undefined)
|
|
115
|
+
uploadData.waveform = metadata.waveform;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Default: in-memory encrypt + upload via Rust, built-in metadata extraction
|
|
120
|
+
const [result] = await Promise.all([
|
|
121
|
+
options.waClient.uploadMedia(buffer, effectiveMediaType),
|
|
122
|
+
extractBuiltInMetadata(buffer, mediaType, uploadData, options)
|
|
123
|
+
]);
|
|
124
|
+
uploadResult = result;
|
|
125
|
+
}
|
|
126
|
+
const obj = WAProto.Message.fromObject({
|
|
127
|
+
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
|
|
128
|
+
url: uploadResult.url,
|
|
129
|
+
directPath: uploadResult.directPath,
|
|
130
|
+
mediaKey: uploadResult.mediaKey,
|
|
131
|
+
fileEncSha256: uploadResult.fileEncSha256,
|
|
132
|
+
fileSha256: uploadResult.fileSha256,
|
|
133
|
+
fileLength: uploadResult.fileLength,
|
|
134
|
+
mediaKeyTimestamp: unixTimestampSeconds(),
|
|
135
|
+
...uploadData,
|
|
136
|
+
media: undefined
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
if (uploadData.ptv) {
|
|
140
|
+
obj.ptvMessage = obj.videoMessage;
|
|
141
|
+
delete obj.videoMessage;
|
|
142
|
+
}
|
|
143
|
+
if (obj.stickerMessage) {
|
|
144
|
+
obj.stickerMessage.stickerSentTs = Date.now();
|
|
145
|
+
}
|
|
146
|
+
if (cacheableKey) {
|
|
147
|
+
logger?.debug({ cacheableKey }, 'set cache');
|
|
148
|
+
await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
|
|
149
|
+
}
|
|
150
|
+
return obj;
|
|
151
|
+
};
|
|
152
|
+
/** Extract metadata (thumbnail, duration, waveform) from buffer using built-in JS libraries */
|
|
153
|
+
async function extractBuiltInMetadata(buffer, mediaType, uploadData, options) {
|
|
154
|
+
try {
|
|
155
|
+
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined';
|
|
156
|
+
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
|
|
157
|
+
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
|
|
158
|
+
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
|
|
159
|
+
if (requiresThumbnailComputation) {
|
|
160
|
+
const { thumbnail, originalImageDimensions } = await generateThumbnail(buffer, mediaType, options);
|
|
161
|
+
uploadData.jpegThumbnail = thumbnail;
|
|
162
|
+
if (!uploadData.width && originalImageDimensions) {
|
|
163
|
+
uploadData.width = originalImageDimensions.width;
|
|
164
|
+
uploadData.height = originalImageDimensions.height;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (requiresDurationComputation) {
|
|
168
|
+
uploadData.seconds = await getAudioDuration(buffer);
|
|
169
|
+
}
|
|
170
|
+
if (requiresWaveformProcessing) {
|
|
171
|
+
uploadData.waveform = await getAudioWaveform(buffer, options.logger);
|
|
172
|
+
}
|
|
173
|
+
if (requiresAudioBackground) {
|
|
174
|
+
uploadData.backgroundArgb = await assertColor(options.backgroundColor);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
options.logger?.warn({ trace: error.stack }, 'failed to obtain extra info');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
|
|
182
|
+
ephemeralExpiration = ephemeralExpiration || 0;
|
|
183
|
+
const content = {
|
|
184
|
+
ephemeralMessage: {
|
|
185
|
+
message: {
|
|
186
|
+
protocolMessage: {
|
|
187
|
+
type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
|
|
188
|
+
ephemeralExpiration
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
return WAProto.Message.fromObject(content);
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Generate forwarded message content like WA does
|
|
197
|
+
* @param message the message to forward
|
|
198
|
+
* @param options.forceForward will show the message as forwarded even if it is from you
|
|
199
|
+
*/
|
|
200
|
+
export const generateForwardMessageContent = (message, forceForward) => {
|
|
201
|
+
let content = message.message;
|
|
202
|
+
if (!content) {
|
|
203
|
+
throw new Boom('no content in message', { statusCode: 400 });
|
|
204
|
+
}
|
|
205
|
+
content = normalizeMessageContent(content);
|
|
206
|
+
// Shallow clone — only the inner message object gets modified (contextInfo)
|
|
207
|
+
content = { ...content };
|
|
208
|
+
let key = Object.keys(content)[0];
|
|
209
|
+
let score = content?.[key]?.contextInfo?.forwardingScore || 0;
|
|
210
|
+
score += message.key.fromMe && !forceForward ? 0 : 1;
|
|
211
|
+
if (key === 'conversation') {
|
|
212
|
+
content.extendedTextMessage = { text: content[key] };
|
|
213
|
+
delete content.conversation;
|
|
214
|
+
key = 'extendedTextMessage';
|
|
215
|
+
}
|
|
216
|
+
const key_ = content?.[key];
|
|
217
|
+
if (score > 0) {
|
|
218
|
+
key_.contextInfo = { forwardingScore: score, isForwarded: true };
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
key_.contextInfo = {};
|
|
222
|
+
}
|
|
223
|
+
return content;
|
|
224
|
+
};
|
|
225
|
+
export const hasNonNullishProperty = (message, key) => {
|
|
226
|
+
return (typeof message === 'object' &&
|
|
227
|
+
message !== null &&
|
|
228
|
+
key in message &&
|
|
229
|
+
message[key] !== null &&
|
|
230
|
+
message[key] !== undefined);
|
|
231
|
+
};
|
|
232
|
+
function hasOptionalProperty(obj, key) {
|
|
233
|
+
return typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null;
|
|
234
|
+
}
|
|
235
|
+
export const generateWAMessageContent = async (message, options) => {
|
|
236
|
+
var _a, _b;
|
|
237
|
+
let m = {};
|
|
238
|
+
if (hasNonNullishProperty(message, 'text')) {
|
|
239
|
+
const extContent = { text: message.text };
|
|
240
|
+
let urlInfo = message.linkPreview;
|
|
241
|
+
if (typeof urlInfo === 'undefined') {
|
|
242
|
+
urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
|
|
243
|
+
}
|
|
244
|
+
if (urlInfo) {
|
|
245
|
+
extContent.matchedText = urlInfo['matched-text'];
|
|
246
|
+
extContent.jpegThumbnail = urlInfo.jpegThumbnail;
|
|
247
|
+
extContent.description = urlInfo.description;
|
|
248
|
+
extContent.title = urlInfo.title;
|
|
249
|
+
extContent.previewType = 0;
|
|
250
|
+
const img = urlInfo.highQualityThumbnail;
|
|
251
|
+
if (img) {
|
|
252
|
+
extContent.thumbnailDirectPath = img.directPath;
|
|
253
|
+
extContent.mediaKey = img.mediaKey;
|
|
254
|
+
extContent.mediaKeyTimestamp = img.mediaKeyTimestamp;
|
|
255
|
+
extContent.thumbnailWidth = img.width;
|
|
256
|
+
extContent.thumbnailHeight = img.height;
|
|
257
|
+
extContent.thumbnailSha256 = img.fileSha256;
|
|
258
|
+
extContent.thumbnailEncSha256 = img.fileEncSha256;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (options.backgroundColor) {
|
|
262
|
+
extContent.backgroundArgb = await assertColor(options.backgroundColor);
|
|
263
|
+
}
|
|
264
|
+
if (options.font) {
|
|
265
|
+
extContent.font = options.font;
|
|
266
|
+
}
|
|
267
|
+
m.extendedTextMessage = extContent;
|
|
268
|
+
}
|
|
269
|
+
else if (hasNonNullishProperty(message, 'contacts')) {
|
|
270
|
+
const contactLen = message.contacts.contacts.length;
|
|
271
|
+
if (!contactLen) {
|
|
272
|
+
throw new Boom('require atleast 1 contact', { statusCode: 400 });
|
|
273
|
+
}
|
|
274
|
+
if (contactLen === 1) {
|
|
275
|
+
m.contactMessage = WAProto.Message.ContactMessage.create(message.contacts.contacts[0]);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
m.contactsArrayMessage = WAProto.Message.ContactsArrayMessage.create(message.contacts);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (hasNonNullishProperty(message, 'location')) {
|
|
282
|
+
m.locationMessage = WAProto.Message.LocationMessage.create(message.location);
|
|
283
|
+
}
|
|
284
|
+
else if (hasNonNullishProperty(message, 'react')) {
|
|
285
|
+
if (!message.react.senderTimestampMs) {
|
|
286
|
+
message.react.senderTimestampMs = Date.now();
|
|
287
|
+
}
|
|
288
|
+
m.reactionMessage = WAProto.Message.ReactionMessage.create(message.react);
|
|
289
|
+
}
|
|
290
|
+
else if (hasNonNullishProperty(message, 'delete')) {
|
|
291
|
+
m.protocolMessage = {
|
|
292
|
+
key: message.delete,
|
|
293
|
+
type: WAProto.Message.ProtocolMessage.Type.REVOKE
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
else if (hasNonNullishProperty(message, 'forward')) {
|
|
297
|
+
m = generateForwardMessageContent(message.forward, message.force);
|
|
298
|
+
}
|
|
299
|
+
else if (hasNonNullishProperty(message, 'disappearingMessagesInChat')) {
|
|
300
|
+
const exp = typeof message.disappearingMessagesInChat === 'boolean'
|
|
301
|
+
? message.disappearingMessagesInChat
|
|
302
|
+
? WA_DEFAULT_EPHEMERAL
|
|
303
|
+
: 0
|
|
304
|
+
: message.disappearingMessagesInChat;
|
|
305
|
+
m = prepareDisappearingMessageSettingContent(exp);
|
|
306
|
+
}
|
|
307
|
+
else if (hasNonNullishProperty(message, 'groupInvite')) {
|
|
308
|
+
m.groupInviteMessage = {};
|
|
309
|
+
m.groupInviteMessage.inviteCode = message.groupInvite.inviteCode;
|
|
310
|
+
m.groupInviteMessage.inviteExpiration = message.groupInvite.inviteExpiration;
|
|
311
|
+
m.groupInviteMessage.caption = message.groupInvite.text;
|
|
312
|
+
m.groupInviteMessage.groupJid = message.groupInvite.jid;
|
|
313
|
+
m.groupInviteMessage.groupName = message.groupInvite.subject;
|
|
314
|
+
//TODO: use built-in interface and get disappearing mode info etc.
|
|
315
|
+
//TODO: cache / use store!?
|
|
316
|
+
if (options.getProfilePicUrl) {
|
|
317
|
+
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
|
|
318
|
+
if (pfpUrl) {
|
|
319
|
+
const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
|
|
320
|
+
if (resp.ok) {
|
|
321
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
322
|
+
m.groupInviteMessage.jpegThumbnail = buf;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (hasNonNullishProperty(message, 'pin')) {
|
|
328
|
+
m.pinInChatMessage = {};
|
|
329
|
+
m.messageContextInfo = {};
|
|
330
|
+
m.pinInChatMessage.key = message.pin;
|
|
331
|
+
m.pinInChatMessage.type = message.type;
|
|
332
|
+
m.pinInChatMessage.senderTimestampMs = Date.now();
|
|
333
|
+
m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
|
|
334
|
+
}
|
|
335
|
+
else if (hasNonNullishProperty(message, 'buttonReply')) {
|
|
336
|
+
switch (message.type) {
|
|
337
|
+
case 'template':
|
|
338
|
+
m.templateButtonReplyMessage = {
|
|
339
|
+
selectedDisplayText: message.buttonReply.displayText,
|
|
340
|
+
selectedId: message.buttonReply.id,
|
|
341
|
+
selectedIndex: message.buttonReply.index
|
|
342
|
+
};
|
|
343
|
+
break;
|
|
344
|
+
case 'plain':
|
|
345
|
+
m.buttonsResponseMessage = {
|
|
346
|
+
selectedButtonId: message.buttonReply.id,
|
|
347
|
+
selectedDisplayText: message.buttonReply.displayText,
|
|
348
|
+
type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
|
|
349
|
+
};
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (hasOptionalProperty(message, 'ptv') && message.ptv) {
|
|
354
|
+
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
|
|
355
|
+
m.ptvMessage = videoMessage;
|
|
356
|
+
}
|
|
357
|
+
else if (hasNonNullishProperty(message, 'product')) {
|
|
358
|
+
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
|
|
359
|
+
m.productMessage = WAProto.Message.ProductMessage.create({
|
|
360
|
+
...message,
|
|
361
|
+
product: {
|
|
362
|
+
...message.product,
|
|
363
|
+
productImage: imageMessage
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
else if (hasNonNullishProperty(message, 'listReply')) {
|
|
368
|
+
m.listResponseMessage = { ...message.listReply };
|
|
369
|
+
}
|
|
370
|
+
else if (hasNonNullishProperty(message, 'event')) {
|
|
371
|
+
m.eventMessage = {};
|
|
372
|
+
const startTime = Math.floor(message.event.startDate.getTime() / 1000);
|
|
373
|
+
if (message.event.call && options.getCallLink) {
|
|
374
|
+
const token = await options.getCallLink(message.event.call, { startTime });
|
|
375
|
+
m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
|
|
376
|
+
}
|
|
377
|
+
m.eventMessage.name = message.event.name;
|
|
378
|
+
m.eventMessage.description = message.event.description;
|
|
379
|
+
m.eventMessage.startTime = startTime;
|
|
380
|
+
m.eventMessage.endTime = message.event.endDate ? Math.floor(message.event.endDate.getTime() / 1000) : undefined;
|
|
381
|
+
m.eventMessage.isCanceled = message.event.isCancelled ?? false;
|
|
382
|
+
m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
|
|
383
|
+
m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
|
|
384
|
+
m.eventMessage.location = message.event.location;
|
|
385
|
+
}
|
|
386
|
+
else if (hasNonNullishProperty(message, 'poll')) {
|
|
387
|
+
(_a = message.poll).selectableCount || (_a.selectableCount = 0);
|
|
388
|
+
(_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
|
|
389
|
+
if (!Array.isArray(message.poll.values)) {
|
|
390
|
+
throw new Boom('Invalid poll values', { statusCode: 400 });
|
|
391
|
+
}
|
|
392
|
+
if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length) {
|
|
393
|
+
throw new Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, {
|
|
394
|
+
statusCode: 400
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const pollCreationMessage = {
|
|
398
|
+
name: message.poll.name,
|
|
399
|
+
selectableOptionsCount: message.poll.selectableCount,
|
|
400
|
+
options: message.poll.values.map(optionName => ({ optionName }))
|
|
401
|
+
};
|
|
402
|
+
if (message.poll.toAnnouncementGroup) {
|
|
403
|
+
// poll v2 is for community announcement groups (single select and multiple)
|
|
404
|
+
m.pollCreationMessageV2 = pollCreationMessage;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
if (message.poll.selectableCount === 1) {
|
|
408
|
+
//poll v3 is for single select polls
|
|
409
|
+
m.pollCreationMessageV3 = pollCreationMessage;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
// poll for multiple choice polls
|
|
413
|
+
m.pollCreationMessage = pollCreationMessage;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
|
|
418
|
+
m.protocolMessage = {
|
|
419
|
+
type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
else if (hasNonNullishProperty(message, 'requestPhoneNumber')) {
|
|
423
|
+
m.requestPhoneNumberMessage = {};
|
|
424
|
+
}
|
|
425
|
+
else if (hasNonNullishProperty(message, 'limitSharing')) {
|
|
426
|
+
m.protocolMessage = {
|
|
427
|
+
type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
|
|
428
|
+
limitSharing: {
|
|
429
|
+
sharingLimited: message.limitSharing === true,
|
|
430
|
+
trigger: 1,
|
|
431
|
+
limitSharingSettingTimestamp: Date.now(),
|
|
432
|
+
initiatedByMe: true
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
m = await prepareWAMessageMedia(message, options);
|
|
438
|
+
}
|
|
439
|
+
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
|
|
440
|
+
m = { viewOnceMessage: { message: m } };
|
|
441
|
+
}
|
|
442
|
+
if ((hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
|
|
443
|
+
(hasOptionalProperty(message, 'mentionAll') && message.mentionAll)) {
|
|
444
|
+
const messageType = Object.keys(m)[0];
|
|
445
|
+
const key = m[messageType];
|
|
446
|
+
if (key && 'contextInfo' in key) {
|
|
447
|
+
key.contextInfo = key.contextInfo || {};
|
|
448
|
+
if (message.mentions?.length) {
|
|
449
|
+
key.contextInfo.mentionedJid = message.mentions;
|
|
450
|
+
}
|
|
451
|
+
if (message.mentionAll) {
|
|
452
|
+
key.contextInfo.nonJidMentions = 1;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (hasOptionalProperty(message, 'edit')) {
|
|
457
|
+
m = {
|
|
458
|
+
protocolMessage: {
|
|
459
|
+
key: message.edit,
|
|
460
|
+
editedMessage: m,
|
|
461
|
+
timestampMs: Date.now(),
|
|
462
|
+
type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
|
|
467
|
+
const messageType = Object.keys(m)[0];
|
|
468
|
+
const key = m[messageType];
|
|
469
|
+
if (key) {
|
|
470
|
+
key.contextInfo = key.contextInfo ? Object.assign(key.contextInfo, message.contextInfo) : message.contextInfo;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return WAProto.Message.create(m);
|
|
474
|
+
};
|
|
475
|
+
export const generateWAMessageFromContent = (jid, message, options) => {
|
|
476
|
+
// set timestamp to now
|
|
477
|
+
// if not specified
|
|
478
|
+
if (!options.timestamp) {
|
|
479
|
+
options.timestamp = new Date();
|
|
480
|
+
}
|
|
481
|
+
const innerMessage = normalizeMessageContent(message);
|
|
482
|
+
const key = getContentType(innerMessage);
|
|
483
|
+
const timestamp = unixTimestampSeconds(options.timestamp);
|
|
484
|
+
const { quoted, userJid } = options;
|
|
485
|
+
if (quoted && !isJidNewsletter(jid)) {
|
|
486
|
+
const participant = quoted.key.fromMe
|
|
487
|
+
? userJid // TODO: Add support for LIDs
|
|
488
|
+
: quoted.participant || quoted.key.participant || quoted.key.remoteJid;
|
|
489
|
+
let quotedMsg = normalizeMessageContent(quoted.message);
|
|
490
|
+
const msgType = getContentType(quotedMsg);
|
|
491
|
+
// strip any redundant properties
|
|
492
|
+
quotedMsg = proto.Message.create({ [msgType]: quotedMsg[msgType] });
|
|
493
|
+
const quotedContent = quotedMsg[msgType];
|
|
494
|
+
if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
|
|
495
|
+
delete quotedContent.contextInfo;
|
|
496
|
+
}
|
|
497
|
+
const contextInfo = ('contextInfo' in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
|
|
498
|
+
contextInfo.participant = jidNormalizedUser(participant);
|
|
499
|
+
contextInfo.stanzaId = quoted.key.id;
|
|
500
|
+
contextInfo.quotedMessage = quotedMsg;
|
|
501
|
+
// if a participant is quoted, then it must be a group
|
|
502
|
+
// hence, remoteJid of group must also be entered
|
|
503
|
+
if (jid !== quoted.key.remoteJid) {
|
|
504
|
+
contextInfo.remoteJid = quoted.key.remoteJid;
|
|
505
|
+
}
|
|
506
|
+
if (contextInfo && innerMessage[key]) {
|
|
507
|
+
/* @ts-ignore */
|
|
508
|
+
innerMessage[key].contextInfo = contextInfo;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (
|
|
512
|
+
// if we want to send a disappearing message
|
|
513
|
+
!!options?.ephemeralExpiration &&
|
|
514
|
+
// and it's not a protocol message -- delete, toggle disappear message
|
|
515
|
+
key !== 'protocolMessage' &&
|
|
516
|
+
// already not converted to disappearing message
|
|
517
|
+
key !== 'ephemeralMessage' &&
|
|
518
|
+
// newsletters don't support ephemeral messages
|
|
519
|
+
!isJidNewsletter(jid)) {
|
|
520
|
+
/* @ts-ignore */
|
|
521
|
+
innerMessage[key].contextInfo = {
|
|
522
|
+
...innerMessage[key].contextInfo,
|
|
523
|
+
expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL
|
|
524
|
+
//ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
message = WAProto.Message.create(message);
|
|
528
|
+
const messageJSON = {
|
|
529
|
+
key: {
|
|
530
|
+
remoteJid: jid,
|
|
531
|
+
fromMe: true,
|
|
532
|
+
id: options?.messageId || '' // Rust generates the real message ID
|
|
533
|
+
},
|
|
534
|
+
message: message,
|
|
535
|
+
messageTimestamp: timestamp,
|
|
536
|
+
messageStubParameters: [],
|
|
537
|
+
participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined, // TODO: Add support for LIDs
|
|
538
|
+
status: WAMessageStatus.PENDING
|
|
539
|
+
};
|
|
540
|
+
return WAProto.WebMessageInfo.fromObject(messageJSON);
|
|
541
|
+
};
|
|
542
|
+
export const generateWAMessage = async (jid, content, options) => {
|
|
543
|
+
options.logger = options?.logger?.child({ msgId: options.messageId });
|
|
544
|
+
options.jid = jid;
|
|
545
|
+
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, options), options);
|
|
546
|
+
};
|
|
547
|
+
/** Get the key to access the true type of content */
|
|
548
|
+
export const getContentType = (content) => {
|
|
549
|
+
if (content) {
|
|
550
|
+
const keys = Object.keys(content);
|
|
551
|
+
const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
|
|
552
|
+
return key;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
/**
|
|
556
|
+
* Normalizes ephemeral, view once messages to regular message content
|
|
557
|
+
* Eg. image messages in ephemeral messages, in view once messages etc.
|
|
558
|
+
* @param content
|
|
559
|
+
* @returns
|
|
560
|
+
*/
|
|
561
|
+
export const normalizeMessageContent = (content) => {
|
|
562
|
+
if (!content) {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
// set max iterations to prevent an infinite loop
|
|
566
|
+
for (let i = 0; i < 5; i++) {
|
|
567
|
+
const inner = getFutureProofMessage(content);
|
|
568
|
+
if (!inner) {
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
content = inner.message;
|
|
572
|
+
}
|
|
573
|
+
return content;
|
|
574
|
+
function getFutureProofMessage(message) {
|
|
575
|
+
return (message?.ephemeralMessage ||
|
|
576
|
+
message?.viewOnceMessage ||
|
|
577
|
+
message?.documentWithCaptionMessage ||
|
|
578
|
+
message?.viewOnceMessageV2 ||
|
|
579
|
+
message?.viewOnceMessageV2Extension ||
|
|
580
|
+
message?.editedMessage ||
|
|
581
|
+
message?.associatedChildMessage ||
|
|
582
|
+
message?.groupStatusMessage ||
|
|
583
|
+
message?.groupStatusMessageV2);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Extract the true message content from a message
|
|
588
|
+
* Eg. extracts the inner message from a disappearing message/view once message
|
|
589
|
+
*/
|
|
590
|
+
export const extractMessageContent = (content) => {
|
|
591
|
+
const extractFromTemplateMessage = (msg) => {
|
|
592
|
+
if (msg.imageMessage) {
|
|
593
|
+
return { imageMessage: msg.imageMessage };
|
|
594
|
+
}
|
|
595
|
+
else if (msg.documentMessage) {
|
|
596
|
+
return { documentMessage: msg.documentMessage };
|
|
597
|
+
}
|
|
598
|
+
else if (msg.videoMessage) {
|
|
599
|
+
return { videoMessage: msg.videoMessage };
|
|
600
|
+
}
|
|
601
|
+
else if (msg.locationMessage) {
|
|
602
|
+
return { locationMessage: msg.locationMessage };
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
return {
|
|
606
|
+
conversation: 'contentText' in msg ? msg.contentText : 'hydratedContentText' in msg ? msg.hydratedContentText : ''
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
content = normalizeMessageContent(content);
|
|
611
|
+
if (content?.buttonsMessage) {
|
|
612
|
+
return extractFromTemplateMessage(content.buttonsMessage);
|
|
613
|
+
}
|
|
614
|
+
if (content?.templateMessage?.hydratedFourRowTemplate) {
|
|
615
|
+
return extractFromTemplateMessage(content?.templateMessage?.hydratedFourRowTemplate);
|
|
616
|
+
}
|
|
617
|
+
if (content?.templateMessage?.hydratedTemplate) {
|
|
618
|
+
return extractFromTemplateMessage(content?.templateMessage?.hydratedTemplate);
|
|
619
|
+
}
|
|
620
|
+
if (content?.templateMessage?.fourRowTemplate) {
|
|
621
|
+
return extractFromTemplateMessage(content?.templateMessage?.fourRowTemplate);
|
|
622
|
+
}
|
|
623
|
+
return content;
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* Downloads the given message. Throws an error if it's not a media message.
|
|
627
|
+
*
|
|
628
|
+
* Uses the Rust bridge for download — provides CDN failover, automatic auth
|
|
629
|
+
* refresh on 401/404, HMAC-SHA256 integrity verification, and AES-256-CBC
|
|
630
|
+
* decryption. Requires `ctx.waClient` (the bridge client).
|
|
631
|
+
*/
|
|
632
|
+
export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
633
|
+
return (await downloadMsg());
|
|
634
|
+
async function downloadMsg() {
|
|
635
|
+
const mContent = extractMessageContent(message.message);
|
|
636
|
+
if (!mContent) {
|
|
637
|
+
throw new Boom('No message present', { statusCode: 400, data: message });
|
|
638
|
+
}
|
|
639
|
+
const contentType = getContentType(mContent);
|
|
640
|
+
let mediaType = String(contentType)?.replace('Message', '');
|
|
641
|
+
const media = mContent[contentType];
|
|
642
|
+
if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
|
|
643
|
+
throw new Boom(`"${String(contentType)}" message is not a media message`);
|
|
644
|
+
}
|
|
645
|
+
if ('thumbnailDirectPath' in media && !('url' in media)) {
|
|
646
|
+
mediaType = 'thumbnail-link';
|
|
647
|
+
}
|
|
648
|
+
if (!('directPath' in media) || !media.directPath || !('mediaKey' in media) || !media.mediaKey) {
|
|
649
|
+
throw new Boom('Media message missing directPath or mediaKey', { statusCode: 400 });
|
|
650
|
+
}
|
|
651
|
+
const mediaObj = media;
|
|
652
|
+
if (!mediaObj.fileSha256 || !mediaObj.fileEncSha256) {
|
|
653
|
+
throw new Boom('Media message missing fileSha256 or fileEncSha256', { statusCode: 400 });
|
|
654
|
+
}
|
|
655
|
+
const args = [
|
|
656
|
+
mediaObj.directPath,
|
|
657
|
+
mediaObj.mediaKey,
|
|
658
|
+
mediaObj.fileSha256,
|
|
659
|
+
mediaObj.fileEncSha256,
|
|
660
|
+
Number(mediaObj.fileLength || 0),
|
|
661
|
+
mediaType
|
|
662
|
+
];
|
|
663
|
+
if (type === 'buffer') {
|
|
664
|
+
const data = await ctx.waClient.downloadMedia(...args);
|
|
665
|
+
return Buffer.from(data);
|
|
666
|
+
}
|
|
667
|
+
// Stream mode: Web ReadableStream from Rust → Node.js Readable
|
|
668
|
+
const webStream = ctx.waClient.downloadMediaStream(...args);
|
|
669
|
+
return Readable.fromWeb(webStream);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* Aggregate votes from a poll message.
|
|
674
|
+
*
|
|
675
|
+
* Takes a poll creation message and its accumulated poll updates (from
|
|
676
|
+
* `messages.update` events) and returns the vote tally per option.
|
|
677
|
+
*
|
|
678
|
+
* `pollUpdates` should contain pre-decrypted votes where `vote.selectedOptions`
|
|
679
|
+
* are the SHA-256 hashes of the voted option names.
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```ts
|
|
683
|
+
* sock.ev.on('messages.update', event => {
|
|
684
|
+
* for (const { key, update } of event) {
|
|
685
|
+
* if (update.pollUpdates) {
|
|
686
|
+
* const pollMsg = await getMessageFromStore(key)
|
|
687
|
+
* const votes = getAggregateVotesInPollMessage({
|
|
688
|
+
* message: pollMsg.message,
|
|
689
|
+
* pollUpdates: pollMsg.pollUpdates
|
|
690
|
+
* })
|
|
691
|
+
* console.log(votes) // [{ name: 'Yes', voters: ['jid1'] }, ...]
|
|
692
|
+
* }
|
|
693
|
+
* }
|
|
694
|
+
* })
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
697
|
+
export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
|
|
698
|
+
const opts = message?.pollCreationMessage?.options ||
|
|
699
|
+
message?.pollCreationMessageV2?.options ||
|
|
700
|
+
message?.pollCreationMessageV3?.options ||
|
|
701
|
+
[];
|
|
702
|
+
// Build hash→name lookup: SHA-256(optionName) hex → optionName
|
|
703
|
+
// Use synchronous hash via crypto.subtle is async, so we pre-build
|
|
704
|
+
// using the raw bytes from selectedOptions and match by position.
|
|
705
|
+
const voteHashMap = {};
|
|
706
|
+
for (const opt of opts) {
|
|
707
|
+
const name = opt.optionName || '';
|
|
708
|
+
voteHashMap[name] = { name, voters: [] };
|
|
709
|
+
}
|
|
710
|
+
for (const update of pollUpdates || []) {
|
|
711
|
+
const { vote } = update;
|
|
712
|
+
if (!vote?.selectedOptions?.length)
|
|
713
|
+
continue;
|
|
714
|
+
const voter = update.pollUpdateMessageKey ? getKeyAuthor(update.pollUpdateMessageKey, meId) : 'unknown';
|
|
715
|
+
for (const optionHash of vote.selectedOptions) {
|
|
716
|
+
const hashHex = Buffer.from(optionHash).toString('hex');
|
|
717
|
+
// Try to find the matching option name
|
|
718
|
+
if (!voteHashMap[hashHex]) {
|
|
719
|
+
voteHashMap[hashHex] = { name: hashHex, voters: [] };
|
|
720
|
+
}
|
|
721
|
+
voteHashMap[hashHex].voters.push(voter);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return Object.values(voteHashMap);
|
|
725
|
+
}
|
|
726
|
+
/** Get the author of a message key */
|
|
727
|
+
function getKeyAuthor(key, meId) {
|
|
728
|
+
return (key.fromMe ? meId : key.participant || key.remoteJid) || 'unknown';
|
|
729
|
+
}
|
|
730
|
+
//# sourceMappingURL=messages.js.map
|