@kangwifi-pro/waliwa 1.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/LICENSE +21 -0
- package/README.md +600 -0
- package/dist/advanced/features.d.ts +211 -0
- package/dist/advanced/features.d.ts.map +1 -0
- package/dist/advanced/features.js +671 -0
- package/dist/advanced/features.js.map +1 -0
- package/dist/auth/auth-state.d.ts +89 -0
- package/dist/auth/auth-state.d.ts.map +1 -0
- package/dist/auth/auth-state.js +330 -0
- package/dist/auth/auth-state.js.map +1 -0
- package/dist/auth/pairing-code.d.ts +78 -0
- package/dist/auth/pairing-code.d.ts.map +1 -0
- package/dist/auth/pairing-code.js +191 -0
- package/dist/auth/pairing-code.js.map +1 -0
- package/dist/auth/qr-code.d.ts +60 -0
- package/dist/auth/qr-code.d.ts.map +1 -0
- package/dist/auth/qr-code.js +151 -0
- package/dist/auth/qr-code.js.map +1 -0
- package/dist/calls/handler.d.ts +61 -0
- package/dist/calls/handler.d.ts.map +1 -0
- package/dist/calls/handler.js +117 -0
- package/dist/calls/handler.js.map +1 -0
- package/dist/core/binary.d.ts +74 -0
- package/dist/core/binary.d.ts.map +1 -0
- package/dist/core/binary.js +448 -0
- package/dist/core/binary.js.map +1 -0
- package/dist/core/crypto.d.ts +60 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +170 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/noise.d.ts +106 -0
- package/dist/core/noise.d.ts.map +1 -0
- package/dist/core/noise.js +307 -0
- package/dist/core/noise.js.map +1 -0
- package/dist/events/emitter.d.ts +44 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +79 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/features/index.d.ts +342 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +755 -0
- package/dist/features/index.js.map +1 -0
- package/dist/fixes/index.d.ts +333 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +762 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/groups/management.d.ts +86 -0
- package/dist/groups/management.d.ts.map +1 -0
- package/dist/groups/management.js +443 -0
- package/dist/groups/management.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/media.d.ts +93 -0
- package/dist/messages/media.d.ts.map +1 -0
- package/dist/messages/media.js +252 -0
- package/dist/messages/media.js.map +1 -0
- package/dist/messages/message-encoder.d.ts +70 -0
- package/dist/messages/message-encoder.d.ts.map +1 -0
- package/dist/messages/message-encoder.js +453 -0
- package/dist/messages/message-encoder.js.map +1 -0
- package/dist/messages/send.d.ts +101 -0
- package/dist/messages/send.d.ts.map +1 -0
- package/dist/messages/send.js +409 -0
- package/dist/messages/send.js.map +1 -0
- package/dist/recovery/index.d.ts +125 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +584 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/skdm/index.d.ts +220 -0
- package/dist/skdm/index.d.ts.map +1 -0
- package/dist/skdm/index.js +600 -0
- package/dist/skdm/index.js.map +1 -0
- package/dist/socket/hybrid.d.ts +118 -0
- package/dist/socket/hybrid.d.ts.map +1 -0
- package/dist/socket/hybrid.js +352 -0
- package/dist/socket/hybrid.js.map +1 -0
- package/dist/socket/wa-socket.d.ts +300 -0
- package/dist/socket/wa-socket.d.ts.map +1 -0
- package/dist/socket/wa-socket.js +1094 -0
- package/dist/socket/wa-socket.js.map +1 -0
- package/dist/socket/ws-socket.d.ts +96 -0
- package/dist/socket/ws-socket.d.ts.map +1 -0
- package/dist/socket/ws-socket.js +302 -0
- package/dist/socket/ws-socket.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/jid.d.ts +46 -0
- package/dist/utils/jid.d.ts.map +1 -0
- package/dist/utils/jid.js +152 -0
- package/dist/utils/jid.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +175 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/API.md +745 -0
- package/docs/ARCHITECTURE.md +307 -0
- package/docs/BAILEYS_FIXES.md +360 -0
- package/docs/FEATURES.md +532 -0
- package/docs/PROTOCOL.md +489 -0
- package/docs/RECOVERY.md +409 -0
- package/docs/SKDM.md +233 -0
- package/examples/ai-bot/index.ts +479 -0
- package/examples/ai-bot/package.json +17 -0
- package/examples/echo-bot/index.ts +171 -0
- package/examples/echo-bot/package.json +18 -0
- package/examples/group-bot/index.ts +557 -0
- package/examples/group-bot/package.json +17 -0
- package/examples/rest-gateway/index.ts +499 -0
- package/examples/rest-gateway/package.json +19 -0
- package/package.json +75 -0
- package/src/advanced/features.ts +817 -0
- package/src/auth/auth-state.ts +342 -0
- package/src/auth/pairing-code.ts +246 -0
- package/src/auth/qr-code.ts +191 -0
- package/src/calls/handler.ts +153 -0
- package/src/core/binary.ts +464 -0
- package/src/core/crypto.ts +189 -0
- package/src/core/noise.ts +406 -0
- package/src/events/emitter.ts +88 -0
- package/src/features/index.ts +921 -0
- package/src/fixes/index.ts +882 -0
- package/src/groups/management.ts +497 -0
- package/src/index.ts +274 -0
- package/src/messages/media.ts +372 -0
- package/src/messages/message-encoder.ts +520 -0
- package/src/messages/send.ts +521 -0
- package/src/recovery/index.ts +704 -0
- package/src/skdm/index.ts +693 -0
- package/src/socket/hybrid.ts +414 -0
- package/src/socket/wa-socket.ts +1347 -0
- package/src/socket/ws-socket.ts +355 -0
- package/src/types/index.ts +457 -0
- package/src/utils/jid.ts +156 -0
- package/src/utils/logger.ts +84 -0
- package/src/utils/retry.ts +205 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Message encoding/decoding
|
|
3
|
+
* Encode MessageContent ke WhatsApp protobuf Message format
|
|
4
|
+
* Decode incoming WA Message ke MessageContent
|
|
5
|
+
*
|
|
6
|
+
* Hybrid approach: manual binary encoding untuk messages (lebih hemat RAM),
|
|
7
|
+
* pakai protobufjs untuk complex nested message types
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
MessageContent,
|
|
12
|
+
WAMessage,
|
|
13
|
+
WAMessageKey,
|
|
14
|
+
JIDLike,
|
|
15
|
+
ContextInfo
|
|
16
|
+
} from '../types/index.js';
|
|
17
|
+
import { concatBytes, getRandomBytes, sha256Hash } from '../core/crypto.js';
|
|
18
|
+
import { jidToString, jidUser, isGroupJid } from '../utils/jid.js';
|
|
19
|
+
|
|
20
|
+
// ============== Message ID Generator ==============
|
|
21
|
+
|
|
22
|
+
let messageCounter = 0;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate unique message ID
|
|
26
|
+
* Format: 3EB0 + 8 random hex chars + counter
|
|
27
|
+
* Contoh: "3EB0ABC1234567890A"
|
|
28
|
+
*/
|
|
29
|
+
export function generateMessageId(): string {
|
|
30
|
+
messageCounter++;
|
|
31
|
+
const rand = getRandomBytes(8);
|
|
32
|
+
const randHex = Array.from(rand).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
33
|
+
const counterHex = messageCounter.toString(16).padStart(2, '0');
|
|
34
|
+
return `3EB0${randHex}${counterHex}`.toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate stanza ID untuk IQ requests
|
|
39
|
+
*/
|
|
40
|
+
export function generateStanzaId(): string {
|
|
41
|
+
const rand = getRandomBytes(4);
|
|
42
|
+
const randHex = Array.from(rand).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
43
|
+
return `${Date.now().toString(36)}-${randHex}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============== Message Encoder ==============
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Encode MessageContent ke WA binary node
|
|
50
|
+
*
|
|
51
|
+
* WhatsApp message structure:
|
|
52
|
+
* <message to="jid" type="text" id="msgId">
|
|
53
|
+
* <conversation>text content</conversation>
|
|
54
|
+
* </message>
|
|
55
|
+
*
|
|
56
|
+
* Atau untuk media:
|
|
57
|
+
* <message to="jid" type="media" id="msgId">
|
|
58
|
+
* <imageMessage>...</imageMessage>
|
|
59
|
+
* </message>
|
|
60
|
+
*/
|
|
61
|
+
export function encodeMessageNode(
|
|
62
|
+
jid: JIDLike,
|
|
63
|
+
content: MessageContent,
|
|
64
|
+
options: EncodeMessageOptions = {}
|
|
65
|
+
): any {
|
|
66
|
+
const id = options.messageId ?? generateMessageId();
|
|
67
|
+
const to = jidToString(jid);
|
|
68
|
+
const type = isGroupJid(jid) ? 'text' : 'text';
|
|
69
|
+
|
|
70
|
+
const messageNode: any = {
|
|
71
|
+
tag: 'message',
|
|
72
|
+
attrs: {
|
|
73
|
+
to,
|
|
74
|
+
type,
|
|
75
|
+
id
|
|
76
|
+
},
|
|
77
|
+
content: [] as any[]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Add participant untuk group messages
|
|
81
|
+
if (options.participant) {
|
|
82
|
+
messageNode.attrs.participant = options.participant;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Determine content type
|
|
86
|
+
if (content.text) {
|
|
87
|
+
messageNode.content.push({
|
|
88
|
+
tag: 'conversation',
|
|
89
|
+
attrs: {},
|
|
90
|
+
content: content.text
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (content.image) {
|
|
95
|
+
messageNode.content.push({
|
|
96
|
+
tag: 'imageMessage',
|
|
97
|
+
attrs: {
|
|
98
|
+
url: content.image.stream ? '' : '',
|
|
99
|
+
mimetype: content.image.mimetype,
|
|
100
|
+
fileSha256: '', // computed on upload
|
|
101
|
+
fileLength: (content.image.fileLength ?? 0).toString()
|
|
102
|
+
},
|
|
103
|
+
content: content.caption ? [{ tag: 'caption', attrs: {}, content: content.caption }] : undefined
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (content.video) {
|
|
108
|
+
messageNode.content.push({
|
|
109
|
+
tag: 'videoMessage',
|
|
110
|
+
attrs: {
|
|
111
|
+
mimetype: content.video.mimetype,
|
|
112
|
+
seconds: (content.video.seconds ?? 0).toString(),
|
|
113
|
+
gifPlayback: content.video.gifPlayback ? 'true' : 'false'
|
|
114
|
+
},
|
|
115
|
+
content: content.caption ? [{ tag: 'caption', attrs: {}, content: content.caption }] : undefined
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (content.audio) {
|
|
120
|
+
messageNode.content.push({
|
|
121
|
+
tag: 'audioMessage',
|
|
122
|
+
attrs: {
|
|
123
|
+
mimetype: content.audio.mimetype,
|
|
124
|
+
ptt: content.audio.ptt ? 'true' : 'false'
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (content.document) {
|
|
130
|
+
messageNode.content.push({
|
|
131
|
+
tag: 'documentMessage',
|
|
132
|
+
attrs: {
|
|
133
|
+
mimetype: content.document.mimetype,
|
|
134
|
+
fileName: content.document.fileName ?? 'document',
|
|
135
|
+
pageCount: content.document.pageCount?.toString() ?? '1'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (content.sticker) {
|
|
141
|
+
messageNode.content.push({
|
|
142
|
+
tag: 'stickerMessage',
|
|
143
|
+
attrs: {
|
|
144
|
+
mimetype: content.sticker.mimetype,
|
|
145
|
+
isAnimated: content.sticker.isAnimated ? 'true' : 'false'
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (content.location) {
|
|
151
|
+
messageNode.content.push({
|
|
152
|
+
tag: 'locationMessage',
|
|
153
|
+
attrs: {
|
|
154
|
+
degreesLatitude: content.location.degreesLatitude.toString(),
|
|
155
|
+
degreesLongitude: content.location.degreesLongitude.toString(),
|
|
156
|
+
name: content.location.name ?? '',
|
|
157
|
+
address: content.location.address ?? ''
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (content.contact) {
|
|
163
|
+
messageNode.content.push({
|
|
164
|
+
tag: 'contactMessage',
|
|
165
|
+
attrs: {
|
|
166
|
+
displayName: content.contact.displayName
|
|
167
|
+
},
|
|
168
|
+
content: content.contact.vcard
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (content.poll) {
|
|
173
|
+
messageNode.content.push({
|
|
174
|
+
tag: 'pollCreationMessage',
|
|
175
|
+
attrs: {
|
|
176
|
+
name: content.poll.name
|
|
177
|
+
},
|
|
178
|
+
content: content.poll.values.map(v => ({
|
|
179
|
+
tag: 'option',
|
|
180
|
+
attrs: { name: v }
|
|
181
|
+
}))
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Quoted message (reply)
|
|
186
|
+
if (content.quoted) {
|
|
187
|
+
const contextInfo = buildContextInfo(content.quoted, content.mentions);
|
|
188
|
+
messageNode.content.push({
|
|
189
|
+
tag: 'contextInfo',
|
|
190
|
+
attrs: {},
|
|
191
|
+
content: contextInfo
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Mentions
|
|
196
|
+
if (content.mentions && content.mentions.length > 0 && !content.quoted) {
|
|
197
|
+
messageNode.content.push({
|
|
198
|
+
tag: 'mentionedJidList',
|
|
199
|
+
attrs: {},
|
|
200
|
+
content: content.mentions.map(m => jidToString(m))
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Ephemeral
|
|
205
|
+
if (content.ephemeralExpiration) {
|
|
206
|
+
messageNode.attrs.ephemeral = content.ephemeralExpiration.toString();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// View once
|
|
210
|
+
if (content.viewOnce) {
|
|
211
|
+
messageNode.attrs.viewOnce = 'true';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return messageNode;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface EncodeMessageOptions {
|
|
218
|
+
messageId?: string;
|
|
219
|
+
participant?: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build context info untuk quoted/reply messages
|
|
224
|
+
*/
|
|
225
|
+
function buildContextInfo(quoted: WAMessage, mentions?: JIDLike[]): any[] {
|
|
226
|
+
const parts: any[] = [];
|
|
227
|
+
parts.push({
|
|
228
|
+
tag: 'stanzaId',
|
|
229
|
+
attrs: {},
|
|
230
|
+
content: quoted.key.id
|
|
231
|
+
});
|
|
232
|
+
parts.push({
|
|
233
|
+
tag: 'participant',
|
|
234
|
+
attrs: {},
|
|
235
|
+
content: jidToString(quoted.key.participant ?? quoted.key.remoteJid)
|
|
236
|
+
});
|
|
237
|
+
if (mentions && mentions.length > 0) {
|
|
238
|
+
parts.push({
|
|
239
|
+
tag: 'mentionedJidList',
|
|
240
|
+
attrs: {},
|
|
241
|
+
content: mentions.map(m => jidToString(m))
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// Quoted message content (partial)
|
|
245
|
+
if (quoted.message?.text) {
|
|
246
|
+
parts.push({
|
|
247
|
+
tag: 'quotedMessage',
|
|
248
|
+
attrs: {},
|
|
249
|
+
content: quoted.message.text
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return parts;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============== Message Decoder ==============
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Decode incoming WA binary node ke WAMessage
|
|
259
|
+
*/
|
|
260
|
+
export function decodeMessageNode(node: any, fromMe: boolean = false): WAMessage | null {
|
|
261
|
+
if (!node || node.tag !== 'message') {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const attrs = node.attrs || {};
|
|
266
|
+
const jid = attrs.from ?? attrs.to ?? '';
|
|
267
|
+
const id = attrs.id;
|
|
268
|
+
const participant = attrs.participant;
|
|
269
|
+
|
|
270
|
+
const key: WAMessageKey = {
|
|
271
|
+
remoteJid: jid,
|
|
272
|
+
fromMe,
|
|
273
|
+
id,
|
|
274
|
+
participant
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const message: MessageContent & { [k: string]: any } = {} as any;
|
|
278
|
+
let messageTimestamp = Date.now();
|
|
279
|
+
let pushName: string | undefined;
|
|
280
|
+
|
|
281
|
+
// Process children
|
|
282
|
+
if (Array.isArray(node.content)) {
|
|
283
|
+
for (const child of node.content) {
|
|
284
|
+
if (typeof child === 'string') continue;
|
|
285
|
+
if (!child.tag) continue;
|
|
286
|
+
|
|
287
|
+
switch (child.tag) {
|
|
288
|
+
case 'conversation':
|
|
289
|
+
message.text = typeof child.content === 'string' ? child.content : '';
|
|
290
|
+
break;
|
|
291
|
+
|
|
292
|
+
case 'imageMessage':
|
|
293
|
+
message.image = {
|
|
294
|
+
stream: undefined as any, // akan di-populate oleh media download
|
|
295
|
+
mimetype: child.attrs?.mimetype ?? 'image/jpeg',
|
|
296
|
+
fileLength: parseInt(child.attrs?.fileLength ?? '0', 10)
|
|
297
|
+
};
|
|
298
|
+
if (child.content && Array.isArray(child.content)) {
|
|
299
|
+
const captionNode = child.content.find((c: any) => c.tag === 'caption');
|
|
300
|
+
if (captionNode) message.caption = captionNode.content;
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
|
|
304
|
+
case 'videoMessage':
|
|
305
|
+
message.video = {
|
|
306
|
+
stream: undefined as any,
|
|
307
|
+
mimetype: child.attrs?.mimetype ?? 'video/mp4',
|
|
308
|
+
seconds: parseInt(child.attrs?.seconds ?? '0', 10),
|
|
309
|
+
gifPlayback: child.attrs?.gifPlayback === 'true'
|
|
310
|
+
};
|
|
311
|
+
if (child.content && Array.isArray(child.content)) {
|
|
312
|
+
const captionNode = child.content.find((c: any) => c.tag === 'caption');
|
|
313
|
+
if (captionNode) message.caption = captionNode.content;
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'audioMessage':
|
|
318
|
+
message.audio = {
|
|
319
|
+
stream: undefined as any,
|
|
320
|
+
mimetype: child.attrs?.mimetype ?? 'audio/mpeg',
|
|
321
|
+
seconds: parseInt(child.attrs?.seconds ?? '0', 10),
|
|
322
|
+
ptt: child.attrs?.ptt === 'true'
|
|
323
|
+
};
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case 'documentMessage':
|
|
327
|
+
message.document = {
|
|
328
|
+
stream: undefined as any,
|
|
329
|
+
mimetype: child.attrs?.mimetype ?? 'application/octet-stream',
|
|
330
|
+
fileName: child.attrs?.fileName ?? 'document',
|
|
331
|
+
pageCount: parseInt(child.attrs?.pageCount ?? '1', 10)
|
|
332
|
+
};
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'stickerMessage':
|
|
336
|
+
message.sticker = {
|
|
337
|
+
stream: undefined as any,
|
|
338
|
+
mimetype: child.attrs?.mimetype ?? 'image/webp',
|
|
339
|
+
isAnimated: child.attrs?.isAnimated === 'true'
|
|
340
|
+
};
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'locationMessage':
|
|
344
|
+
message.location = {
|
|
345
|
+
degreesLatitude: parseFloat(child.attrs?.degreesLatitude ?? '0'),
|
|
346
|
+
degreesLongitude: parseFloat(child.attrs?.degreesLongitude ?? '0'),
|
|
347
|
+
name: child.attrs?.name,
|
|
348
|
+
address: child.attrs?.address
|
|
349
|
+
};
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case 'contactMessage':
|
|
353
|
+
message.contact = {
|
|
354
|
+
displayName: child.attrs?.displayName ?? '',
|
|
355
|
+
vcard: typeof child.content === 'string' ? child.content : ''
|
|
356
|
+
};
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
case 'pollCreationMessage':
|
|
360
|
+
message.poll = {
|
|
361
|
+
name: child.attrs?.name ?? '',
|
|
362
|
+
values: Array.isArray(child.content)
|
|
363
|
+
? child.content.filter((c: any) => c.tag === 'option').map((c: any) => c.attrs?.name)
|
|
364
|
+
: []
|
|
365
|
+
};
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case 'contextInfo':
|
|
369
|
+
// Process quoted message
|
|
370
|
+
if (Array.isArray(child.content)) {
|
|
371
|
+
const stanzaId = child.content.find((c: any) => c.tag === 'stanzaId');
|
|
372
|
+
const participantNode = child.content.find((c: any) => c.tag === 'participant');
|
|
373
|
+
const mentionedJids = child.content.find((c: any) => c.tag === 'mentionedJidList');
|
|
374
|
+
// Build quoted message stub
|
|
375
|
+
if (stanzaId) {
|
|
376
|
+
message.quoted = {
|
|
377
|
+
key: {
|
|
378
|
+
remoteJid: jid,
|
|
379
|
+
fromMe: false,
|
|
380
|
+
id: stanzaId.content
|
|
381
|
+
},
|
|
382
|
+
message: {},
|
|
383
|
+
messageTimestamp: 0,
|
|
384
|
+
status: 'read' as const
|
|
385
|
+
} as WAMessage;
|
|
386
|
+
if (participantNode) {
|
|
387
|
+
message.quoted!.key.participant = participantNode.content;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (mentionedJids && Array.isArray(mentionedJids.content)) {
|
|
391
|
+
message.mentions = mentionedJids.content;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
case 'pushName':
|
|
397
|
+
pushName = typeof child.content === 'string' ? child.content : undefined;
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case 'deviceSentTime':
|
|
401
|
+
case 'timestamp':
|
|
402
|
+
const ts = child.attrs?.value ?? child.content;
|
|
403
|
+
if (ts) messageTimestamp = parseInt(String(ts), 10);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Determine status from attrs
|
|
410
|
+
const status = attrs.type === 'media' ? 'sent' : 'pending';
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
key,
|
|
414
|
+
message,
|
|
415
|
+
messageTimestamp,
|
|
416
|
+
pushName,
|
|
417
|
+
status: status as any
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ============== Receipt Encoder ==============
|
|
422
|
+
|
|
423
|
+
export type ReceiptType = 'read' | 'receipt' | 'played' | 'inactive';
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Build receipt node untuk send read/delivered confirmation
|
|
427
|
+
*/
|
|
428
|
+
export function buildReceiptNode(
|
|
429
|
+
jid: JIDLike,
|
|
430
|
+
messageIds: string[],
|
|
431
|
+
type: ReceiptType = 'read',
|
|
432
|
+
participant?: JIDLike
|
|
433
|
+
): any {
|
|
434
|
+
return {
|
|
435
|
+
tag: 'receipt',
|
|
436
|
+
attrs: {
|
|
437
|
+
id: messageIds[0],
|
|
438
|
+
type,
|
|
439
|
+
to: jidToString(jid),
|
|
440
|
+
t: Math.floor(Date.now() / 1000).toString()
|
|
441
|
+
},
|
|
442
|
+
content: messageIds.map(id => ({
|
|
443
|
+
tag: 'receipt',
|
|
444
|
+
attrs: { id }
|
|
445
|
+
}))
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============== Status Encoder ==============
|
|
450
|
+
|
|
451
|
+
export type MessageStatusUpdateType = 'read' | 'delivered' | 'played';
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Build status node untuk update message status (read receipts)
|
|
455
|
+
*/
|
|
456
|
+
export function buildStatusNode(
|
|
457
|
+
jid: JIDLike,
|
|
458
|
+
messageId: string,
|
|
459
|
+
type: MessageStatusUpdateType,
|
|
460
|
+
participant?: JIDLike
|
|
461
|
+
): any {
|
|
462
|
+
const attrs: any = {
|
|
463
|
+
type,
|
|
464
|
+
to: jidToString(jid),
|
|
465
|
+
id: messageId
|
|
466
|
+
};
|
|
467
|
+
if (participant) attrs.participant = jidToString(participant);
|
|
468
|
+
return {
|
|
469
|
+
tag: 'status',
|
|
470
|
+
attrs,
|
|
471
|
+
content: undefined
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============== Helpers ==============
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Extract text content dari message
|
|
479
|
+
*/
|
|
480
|
+
export function extractMessageText(message: any): string {
|
|
481
|
+
if (!message) return '';
|
|
482
|
+
if (typeof message.text === 'string') return message.text;
|
|
483
|
+
if (typeof message.conversation === 'string') return message.conversation;
|
|
484
|
+
if (message.caption) return message.caption;
|
|
485
|
+
return '';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Check apakah message punya media
|
|
490
|
+
*/
|
|
491
|
+
export function hasMedia(message: any): boolean {
|
|
492
|
+
return !!(message?.image || message?.video || message?.audio || message?.document || message?.sticker);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get media info dari message
|
|
497
|
+
*/
|
|
498
|
+
export function getMediaInfo(message: any): {
|
|
499
|
+
type: 'image' | 'video' | 'audio' | 'document' | 'sticker';
|
|
500
|
+
mimetype: string;
|
|
501
|
+
fileName?: string;
|
|
502
|
+
fileLength?: number;
|
|
503
|
+
} | null {
|
|
504
|
+
if (message?.image) {
|
|
505
|
+
return { type: 'image', mimetype: message.image.mimetype, fileLength: message.image.fileLength };
|
|
506
|
+
}
|
|
507
|
+
if (message?.video) {
|
|
508
|
+
return { type: 'video', mimetype: message.video.mimetype, fileLength: message.video.fileLength };
|
|
509
|
+
}
|
|
510
|
+
if (message?.audio) {
|
|
511
|
+
return { type: 'audio', mimetype: message.audio.mimetype, fileLength: message.audio.fileLength };
|
|
512
|
+
}
|
|
513
|
+
if (message?.document) {
|
|
514
|
+
return { type: 'document', mimetype: message.document.mimetype, fileName: message.document.fileName };
|
|
515
|
+
}
|
|
516
|
+
if (message?.sticker) {
|
|
517
|
+
return { type: 'sticker', mimetype: message.sticker.mimetype };
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|