@prototypevip/baileys 0.1.4 → 0.1.6

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.
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Prototype VIP Baileys Helpers (0.1.6)
3
+ *
4
+ * طبقة helpers سهلة فوق المسارات الأصلية الموجودة في المكتبة
5
+ * (sock.sendMessage → nativeFlow / album / code / event / product ...).
6
+ * مفيش هنا أي بروتوكول جديد: كل دالة بتفوّض لمسار مجرَّب جوه المكتبة.
7
+ */
8
+
9
+ import { proto } from "../../WAProto/index.js";
10
+ import { generateWAMessageFromContent } from "./messages.js";
11
+ import { wrapViewOnce } from "./view-once.js";
12
+ // الكلاسات دي مصدَّرة أصلاً من message-builder.js عن طريق Utils/index.js
13
+ import { Button, ButtonV2, Carousel, AIRich, Toolkit } from "./message-builder.js";
14
+
15
+ /* ───────────────────────────── internals ───────────────────────────── */
16
+
17
+ const MEDIA_KEYS = ["image", "video", "document"];
18
+
19
+ const hasMedia = (obj) => MEDIA_KEYS.some((k) => obj?.[k]);
20
+
21
+ const isPrivateJid = (jid) =>
22
+ typeof jid === "string" && (jid.endsWith("@s.whatsapp.net") || jid.endsWith("@lid"));
23
+
24
+ const assertSock = (sock, method = "sendMessage") => {
25
+ if (!sock || typeof sock[method] !== "function") {
26
+ throw new Error(`Invalid socket: sock.${method} is not a function`);
27
+ }
28
+ };
29
+
30
+ /** View-Once من content.viewOnce أو options.viewOnce (true | "v2" | "force" | {version,force}) */
31
+ const pickViewOnce = (content, options) => content?.viewOnce ?? options?.viewOnce;
32
+
33
+ const MIME_BY_EXT = {
34
+ pdf: "application/pdf",
35
+ zip: "application/zip",
36
+ rar: "application/vnd.rar",
37
+ "7z": "application/x-7z-compressed",
38
+ txt: "text/plain",
39
+ json: "application/json",
40
+ csv: "text/csv",
41
+ html: "text/html",
42
+ js: "text/javascript",
43
+ apk: "application/vnd.android.package-archive",
44
+ doc: "application/msword",
45
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
46
+ xls: "application/vnd.ms-excel",
47
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
48
+ ppt: "application/vnd.ms-powerpoint",
49
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
50
+ mp3: "audio/mpeg",
51
+ mp4: "video/mp4",
52
+ png: "image/png",
53
+ jpg: "image/jpeg",
54
+ jpeg: "image/jpeg",
55
+ webp: "image/webp",
56
+ };
57
+
58
+ const mimeFromName = (name = "") => MIME_BY_EXT[String(name).split("?")[0].split(".").pop()?.toLowerCase()] || "application/octet-stream";
59
+
60
+ const defaultRowId = (label) => `#${String(label || "row").trim().replace(/\s+/g, "_")}`;
61
+
62
+ /**
63
+ * يحوّل أي شكل مدعوم لزر إلى الصيغة اللي بتفهمها المكتبة في { nativeFlow: [...] }:
64
+ * { text, id } | { text, url } | { text, copy } | { text, call } | { text, sections }
65
+ * ويقبل كمان الصيغ البديلة: rowId / display_text / cta_url / copy_code / phone_number
66
+ * والصيغة الخام { name, buttonParamsJson } بتعدّي زي ما هي.
67
+ */
68
+ function normalizeButton(btn) {
69
+ if (!btn || typeof btn !== "object") {
70
+ throw new TypeError("Each button must be an object");
71
+ }
72
+ if (btn.name && btn.buttonParamsJson) return btn; // native format
73
+
74
+ const text = btn.text ?? btn.display_text ?? btn.buttonText ?? btn.title;
75
+ const out = { text };
76
+ if (btn.icon) out.icon = btn.icon;
77
+
78
+ const id = btn.id ?? btn.rowId;
79
+ const url = btn.url ?? btn.cta_url;
80
+ const copy = btn.copy ?? btn.copy_code;
81
+ const call = btn.call ?? btn.phone_number;
82
+
83
+ if (id) out.id = id;
84
+ else if (copy) out.copy = String(copy);
85
+ else if (url) {
86
+ out.url = url;
87
+ if (btn.useWebview) out.useWebview = btn.useWebview;
88
+ } else if (call) out.call = String(call);
89
+ else if (Array.isArray(btn.sections)) out.sections = btn.sections;
90
+ else out.id = defaultRowId(text || "btn"); // default: quick_reply
91
+ return out;
92
+ }
93
+
94
+ /* ───────────────────────────── buttons ───────────────────────────── */
95
+
96
+ /**
97
+ * يبني أزرار Native Flow بالصيغة الخام ({ name, buttonParamsJson })
98
+ * لو عاوز تبعتها مباشرة في interactiveMessage.nativeFlowMessage.buttons.
99
+ */
100
+ export function buildNativeFlowButtons(buttons = []) {
101
+ return buttons.map((raw) => {
102
+ const btn = normalizeButton(raw);
103
+ if (btn.name && btn.buttonParamsJson) return btn;
104
+ const label = btn.text;
105
+ if (btn.id) {
106
+ return { name: "quick_reply", buttonParamsJson: JSON.stringify({ display_text: label || "Button", id: btn.id }) };
107
+ }
108
+ if (btn.copy) {
109
+ return { name: "cta_copy", buttonParamsJson: JSON.stringify({ display_text: label || "Copy", copy_code: btn.copy }) };
110
+ }
111
+ if (btn.url) {
112
+ return {
113
+ name: "cta_url",
114
+ buttonParamsJson: JSON.stringify({ display_text: label || "Open", url: btn.url, merchant_url: btn.url }),
115
+ };
116
+ }
117
+ if (btn.call) {
118
+ return { name: "cta_call", buttonParamsJson: JSON.stringify({ display_text: label || "Call", phone_number: btn.call }) };
119
+ }
120
+ return { name: "single_select", buttonParamsJson: JSON.stringify({ title: label || "Select", sections: btn.sections }) };
121
+ });
122
+ }
123
+
124
+ /**
125
+ * أزرار Native Flow (تفوّض لـ sock.sendMessage → { nativeFlow })
126
+ *
127
+ * await sendButtons(sock, jid, {
128
+ * text: "اختار", footer: "Prototype VIP",
129
+ * buttons: [{ text:"Menu", id:"#menu" }, { text:"Site", url:"https://..." },
130
+ * { text:"Copy", copy:"CODE" }, { text:"Call", call:"2012..." }],
131
+ * // اختياري: image | video | document (بيتحوّل لـ header)، title، subtitle، ai: true
132
+ * }, { quoted: m })
133
+ */
134
+ export async function sendButtons(sock, jid, content = {}, options = {}) {
135
+ assertSock(sock);
136
+ const { text, caption, buttons, ai, ...rest } = content;
137
+
138
+ if (!Array.isArray(buttons) || buttons.length === 0) {
139
+ throw new Error("sendButtons requires a non-empty buttons array");
140
+ }
141
+
142
+ const payload = { ...rest, nativeFlow: buttons.map(normalizeButton) };
143
+
144
+ // مسار nativeFlow في المكتبة بيستخدم text للنص العادي و caption مع الميديا
145
+ if (hasMedia(rest)) {
146
+ payload.caption = caption ?? text ?? "";
147
+ } else {
148
+ payload.text = text ?? caption ?? "";
149
+ }
150
+
151
+ // أيقونة الـ AI: sendMessage بيقبلها كـ content.ai وفي الشات الخاص بس
152
+ const wantAi = ai ?? options.ai ?? options.AI;
153
+ if (wantAi && isPrivateJid(jid)) payload.ai = true;
154
+
155
+ const { ai: _a, AI: _b, ...sendOptions } = options;
156
+ return sock.sendMessage(jid, payload, sendOptions);
157
+ }
158
+
159
+ /**
160
+ * قائمة اختيار. الافتراضي: single_select داخل Native Flow (بيشتغل على الأجهزة الحديثة).
161
+ * مرّر legacy: true لو عاوز listMessage الكلاسيكي.
162
+ */
163
+ export async function sendList(sock, jid, content = {}, options = {}) {
164
+ assertSock(sock);
165
+ const { text = "", footer, title = "", buttonText = "Select", sections, legacy = false, ...rest } = content;
166
+
167
+ if (!Array.isArray(sections) || sections.length === 0) {
168
+ throw new Error("sendList requires a non-empty sections array");
169
+ }
170
+
171
+ if (legacy) {
172
+ const msg = generateWAMessageFromContent(
173
+ jid,
174
+ wrapViewOnce({
175
+ listMessage: {
176
+ title,
177
+ description: text,
178
+ buttonText,
179
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
180
+ footerText: footer || undefined,
181
+ sections: sections.map((sec) => ({
182
+ title: sec.title || "",
183
+ rows: (sec.rows || []).map((row) => ({
184
+ title: row.title || row.text || "",
185
+ description: row.description || "",
186
+ rowId: row.rowId || row.id || defaultRowId(row.title || row.text),
187
+ })),
188
+ })),
189
+ },
190
+ }, pickViewOnce(content, options)),
191
+ { userJid: sock.user?.id, quoted: options.quoted, messageId: options.messageId }
192
+ );
193
+ const additionalNodes = isPrivateJid(jid)
194
+ ? [{ tag: "biz", attrs: {}, content: [{ tag: "list", attrs: { v: "2", type: "product_list" } }] }]
195
+ : [];
196
+ await sock.relayMessage(jid, msg.message, {
197
+ messageId: msg.key.id,
198
+ additionalNodes: [...(options.additionalNodes || []), ...additionalNodes],
199
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
200
+ statusJidList: options.statusJidList,
201
+ });
202
+ return msg;
203
+ }
204
+
205
+ const nfSections = sections.map((sec) => ({
206
+ title: sec.title || "",
207
+ ...(sec.highlight_label ? { highlight_label: sec.highlight_label } : {}),
208
+ rows: (sec.rows || []).map((row) => ({
209
+ header: row.header || "",
210
+ title: row.title || row.text || "",
211
+ description: row.description || "",
212
+ id: row.id || row.rowId || defaultRowId(row.title || row.text),
213
+ })),
214
+ }));
215
+
216
+ return sendButtons(
217
+ sock,
218
+ jid,
219
+ { ...rest, text, footer, title, buttons: [{ text: buttonText, sections: nfSections }] },
220
+ options
221
+ );
222
+ }
223
+
224
+ /** اختصار: أزرار رد سريع من نصوص بس */
225
+ export async function sendQuickButtons(sock, jid, text, buttonLabels = [], options = {}) {
226
+ const buttons = buttonLabels.map((label, i) => ({
227
+ text: label,
228
+ id: `#btn_${i + 1}`,
229
+ }));
230
+ return sendButtons(sock, jid, { text, buttons }, options);
231
+ }
232
+
233
+ /** رسالة بأيقونة AI (شات خاص فقط — في الجروبات بتتبعت عادي من غير الأيقونة) */
234
+ export async function sendAIMessage(sock, jid, content, options = {}) {
235
+ const base = typeof content === "string" ? { text: content } : { ...content };
236
+ if (Array.isArray(base.buttons) && base.buttons.length) {
237
+ return sendButtons(sock, jid, { ...base, ai: true }, options);
238
+ }
239
+ assertSock(sock);
240
+ const { ai: _a, AI: _b, ...sendOptions } = options;
241
+ return sock.sendMessage(jid, isPrivateJid(jid) ? { ...base, ai: true } : base, sendOptions);
242
+ }
243
+
244
+ /* ───────────────────────────── album ───────────────────────────── */
245
+
246
+ /**
247
+ * ألبوم صور/فيديوهات. المكتبة بتتوقع album كمصفوفة، وبتقرأ expectedImageCount/
248
+ * expectedVideoCount من نفس المصفوفة، فبنضيفهم عليها.
249
+ *
250
+ * await sendAlbum(sock, jid, [{ image:{url}, caption:"1" }, { video:{url} }], { quoted: m, delayMs: 1500 })
251
+ */
252
+ export async function sendAlbum(sock, jid, mediaItems = [], options = {}) {
253
+ assertSock(sock);
254
+ if (!Array.isArray(mediaItems) || mediaItems.length === 0) {
255
+ throw new Error("sendAlbum requires a non-empty array of media items");
256
+ }
257
+ for (const [i, item] of mediaItems.entries()) {
258
+ if (!item || (!item.image && !item.video)) {
259
+ throw new Error(`sendAlbum: item [${i}] must contain image or video`);
260
+ }
261
+ }
262
+ const album = Object.assign([...mediaItems], {
263
+ expectedImageCount: mediaItems.filter((i) => i.image).length,
264
+ expectedVideoCount: mediaItems.filter((i) => i.video).length,
265
+ });
266
+ const { viewOnce, ...sendOptions } = options;
267
+ return sock.sendMessage(jid, { album, ...(viewOnce ? { viewOnce } : {}) }, sendOptions);
268
+ }
269
+
270
+ /* ───────────────────────────── carousel ───────────────────────────── */
271
+
272
+ /**
273
+ * Carousel حقيقي عن طريق كلاسات المكتبة (Button.toCard + Carousel).
274
+ * كل كارت لازم يكون له image أو video.
275
+ *
276
+ * await sendCarousel(sock, jid, {
277
+ * text: "اختار منتج", footer: "Prototype VIP",
278
+ * cards: [{ title:"منتج 1", body:"وصف", image:"https://..." (أو Buffer), buttons:[{ text:"اشتري", id:"#buy1" }] }]
279
+ * })
280
+ */
281
+ export async function sendCarousel(sock, jid, content = {}, options = {}) {
282
+ assertSock(sock, "relayMessage");
283
+ const { text = "", footer = "", cards } = content;
284
+ if (!Array.isArray(cards) || cards.length === 0) {
285
+ throw new Error("sendCarousel requires a non-empty cards array");
286
+ }
287
+
288
+ const carousel = new Carousel(sock).setBody(text);
289
+ if (footer) carousel.setFooter(footer);
290
+ const viewOnce = pickViewOnce(content, options);
291
+ if (viewOnce) carousel.setViewOnce(viewOnce);
292
+
293
+ for (const [i, card] of cards.entries()) {
294
+ const media = card.image ?? card.video;
295
+ if (!media) throw new Error(`sendCarousel: card [${i}] must include image or video`);
296
+
297
+ const b = new Button(sock);
298
+ if (card.title) b.setTitle(card.title);
299
+ if (card.subtitle) b.setSubtitle(card.subtitle);
300
+ b.setBody(card.body || card.caption || card.title || " ");
301
+ if (card.footer) b.setFooter(card.footer);
302
+
303
+ const toData = (m, kind) => (Buffer.isBuffer(m) ? { [kind]: m } : typeof m === "string" ? { [kind]: { url: m } } : { [kind]: m });
304
+ b.setMedia(card.image ? toData(card.image, "image") : toData(card.video, "video"));
305
+
306
+ for (const nb of buildNativeFlowButtons(card.buttons || [])) {
307
+ b.addButton(nb.name, nb.buttonParamsJson);
308
+ }
309
+ carousel.addCard(await b.toCard());
310
+ }
311
+
312
+ const { viewOnce: _vo, ...sendOptions } = options;
313
+ return carousel.send(jid, { userJid: sock.user?.id, ...sendOptions });
314
+ }
315
+
316
+ /* ───────────────────────────── rich (AI) ───────────────────────────── */
317
+
318
+ /**
319
+ * ⚠️ مهم: واتساب/المكتبة مفيهاش render حقيقي لـ HTML داخل الفقاعة.
320
+ * اللي بيحصل فعلياً: بتتبعت رسالة Rich Response (شكل Meta AI) والـ HTML
321
+ * بيظهر كـ code block ملوّن (syntax highlight) — ده المسار الوحيد المدعوم
322
+ * والمجرَّب في المكتبة (prepareRichResponseMessage).
323
+ */
324
+ export async function sendRichHtml(sock, jid, content, options = {}) {
325
+ assertSock(sock);
326
+ const { title, html = "", summary, footer } = typeof content === "string" ? { html: content } : content || {};
327
+ if (!html) throw new Error("sendRichHtml requires html");
328
+
329
+ return sock.sendMessage(
330
+ jid,
331
+ {
332
+ ...(title ? { headerText: title } : {}),
333
+ ...(summary ? { contentText: summary } : {}),
334
+ code: html,
335
+ language: "html",
336
+ ...(footer ? { footerText: footer } : {}),
337
+ },
338
+ options
339
+ );
340
+ }
341
+
342
+ /** alias قديم — نفس sendRichHtml (اتشال منه المسار الوهمي AIRich.html) */
343
+ export async function sendRichHtmlAdvanced(sock, jid, content, options = {}) {
344
+ return sendRichHtml(sock, jid, content, options);
345
+ }
346
+
347
+ /** رسالة Rich بكود ملوّن بأي لغة */
348
+ export async function sendRichCode(sock, jid, { code, language = "javascript", title, summary, footer } = {}, options = {}) {
349
+ assertSock(sock);
350
+ if (!code) throw new Error("sendRichCode requires code");
351
+ return sock.sendMessage(
352
+ jid,
353
+ {
354
+ ...(title ? { headerText: title } : {}),
355
+ ...(summary ? { contentText: summary } : {}),
356
+ code,
357
+ language,
358
+ ...(footer ? { footerText: footer } : {}),
359
+ },
360
+ options
361
+ );
362
+ }
363
+
364
+ /* ───────────────────────────── misc senders ───────────────────────────── */
365
+
366
+ /**
367
+ * View-Once لأي نوع رسالة (0.1.6) — مش صورة/فيديو/صوت بس:
368
+ * نص، ملف، أزرار، قائمة، كاروسيل، ألبوم، منتج، لوكيشن، جهة اتصال، Rich ...
369
+ *
370
+ * await sendViewOnce(sock, jid, { image: { url }, caption: "x" })
371
+ * await sendViewOnce(sock, jid, { text: "x", buttons: [...] }) // → sendButtons
372
+ * await sendViewOnce(sock, jid, { text: "x", sections: [...] }) // → sendList
373
+ * await sendViewOnce(sock, jid, { text: "x", cards: [...] }) // → sendCarousel
374
+ * await sendViewOnce(sock, jid, [{ image }, { video }]) // → sendAlbum
375
+ * options.viewOnce = true | "v2" | "force" (الافتراضي true)
376
+ */
377
+ export async function sendViewOnce(sock, jid, content, options = {}) {
378
+ assertSock(sock);
379
+ const flag = (typeof content === "object" && !Array.isArray(content) && content?.viewOnce) || options.viewOnce || true;
380
+ const opts = { ...options, viewOnce: flag };
381
+
382
+ if (Array.isArray(content)) return sendAlbum(sock, jid, content, opts);
383
+
384
+ const payload = typeof content === "string" ? { text: content } : { ...content };
385
+ delete payload.viewOnce;
386
+
387
+ if (Array.isArray(payload.cards)) return sendCarousel(sock, jid, payload, opts);
388
+ if (Array.isArray(payload.sections)) return sendList(sock, jid, payload, opts);
389
+ if (Array.isArray(payload.buttons) && payload.buttons.length) return sendButtons(sock, jid, payload, opts);
390
+ return sock.sendMessage(jid, { ...payload, viewOnce: flag }, options);
391
+ }
392
+
393
+ /* ───────────────────────────── extras (0.1.6) ───────────────────────────── */
394
+
395
+ /**
396
+ * ملف/مستند. source: { url } | Buffer | رابط نصي | { document, fileName, mimetype, caption }
397
+ * await sendDocument(sock, jid, { url: "https://x/a.pdf", fileName: "a.pdf", caption: "تفضل" }, { viewOnce: true })
398
+ */
399
+ export async function sendDocument(sock, jid, content = {}, options = {}) {
400
+ assertSock(sock);
401
+ const src = typeof content === "string" ? { url: content } : Buffer.isBuffer(content) ? { document: content } : { ...content };
402
+ const doc = src.document ?? src.file ?? src.buffer ?? (src.url ? { url: src.url } : undefined);
403
+ if (!doc) throw new Error("sendDocument requires document | file | buffer | url");
404
+ const document = typeof doc === "string" ? { url: doc } : doc;
405
+ const urlLike = document?.url || (typeof src.url === "string" ? src.url : "");
406
+ const fileName = src.fileName || src.filename || src.name || (urlLike ? String(urlLike).split("?")[0].split("/").pop() : "file");
407
+ const mimetype = src.mimetype || src.mime || mimeFromName(fileName);
408
+ const { caption, text } = src;
409
+ return sock.sendMessage(
410
+ jid,
411
+ { document, fileName, mimetype, ...(caption || text ? { caption: caption ?? text } : {}) },
412
+ options
413
+ );
414
+ }
415
+
416
+ /** تعديل رسالة بعتّها قبل كده (نص/كابشن) */
417
+ export async function sendEdit(sock, jid, key, content, options = {}) {
418
+ assertSock(sock);
419
+ if (!key) throw new Error("sendEdit requires the original message key");
420
+ const payload = typeof content === "string" ? { text: content } : { ...content };
421
+ return sock.sendMessage(jid, { ...payload, edit: key }, options);
422
+ }
423
+
424
+ /** حذف رسالة للكل */
425
+ export async function sendDelete(sock, jid, key) {
426
+ assertSock(sock);
427
+ if (!key) throw new Error("sendDelete requires the message key");
428
+ return sock.sendMessage(jid, { delete: key });
429
+ }
430
+
431
+ /**
432
+ * يبعت بعد ما يعمل "بيكتب..." (أو "بيسجل..." لو recording) لمدة typingMs
433
+ * await sendWithTyping(sock, jid, { text: "أهلا" }, { typingMs: 1200 })
434
+ */
435
+ export async function sendWithTyping(sock, jid, content, options = {}) {
436
+ assertSock(sock);
437
+ const { typingMs = 1000, presence = "composing", ...sendOptions } = options;
438
+ const canPresence = typeof sock.sendPresenceUpdate === "function";
439
+ try {
440
+ if (canPresence) {
441
+ await sock.sendPresenceUpdate(presence, jid);
442
+ await new Promise((r) => setTimeout(r, Math.max(0, Number(typingMs) || 0)));
443
+ }
444
+ const payload = typeof content === "string" ? { text: content } : content;
445
+ return await sock.sendMessage(jid, payload, sendOptions);
446
+ } finally {
447
+ if (canPresence) await sock.sendPresenceUpdate("paused", jid).catch(() => {});
448
+ }
449
+ }
450
+
451
+ export async function sendReaction(sock, jid, key, emoji = "👍") {
452
+ assertSock(sock);
453
+ return sock.sendMessage(jid, { react: { text: emoji, key } });
454
+ }
455
+
456
+ export async function sendPoll(sock, jid, name, values = [], options = {}) {
457
+ assertSock(sock);
458
+ const { selectableCount = 1, ...sendOptions } = options;
459
+ return sock.sendMessage(jid, { poll: { name, values, selectableCount } }, sendOptions);
460
+ }
461
+
462
+ /** منشن لكل أعضاء الجروب (بيرمي خطأ لو معرفش يجيب الأعضاء بدل ما يبعت من غير منشن) */
463
+ export async function sendMentionAll(sock, jid, text, options = {}) {
464
+ assertSock(sock);
465
+ if (!jid.endsWith("@g.us")) throw new Error("sendMentionAll only works in groups");
466
+ const meta = await sock.groupMetadata(jid);
467
+ const mentions = (meta.participants || []).map((p) => p.id).filter(Boolean);
468
+ return sock.sendMessage(jid, { text, mentions }, options);
469
+ }
470
+
471
+ /** options: { name, address, ...sendOptions } */
472
+ export async function sendLocation(sock, jid, degreesLatitude, degreesLongitude, options = {}) {
473
+ assertSock(sock);
474
+ const { name = "", address = "", ...sendOptions } = options;
475
+ return sock.sendMessage(
476
+ jid,
477
+ { location: { degreesLatitude, degreesLongitude, name, address } },
478
+ sendOptions
479
+ );
480
+ }
481
+
482
+ /** contacts: { fullName|name, phone|number, organization|org } أو مصفوفة منهم */
483
+ export async function sendContact(sock, jid, contacts, options = {}) {
484
+ assertSock(sock);
485
+ const clean = (s) => String(s ?? "").replace(/[\r\n;,\\]/g, " ").trim();
486
+ const list = (Array.isArray(contacts) ? contacts : [contacts]).map((c) => {
487
+ const phone = String(c.phone || c.number || "").replace(/\D/g, "");
488
+ if (!phone) throw new Error("sendContact: phone is required");
489
+ const name = clean(c.fullName || c.name || phone);
490
+ const org = clean(c.organization || c.org);
491
+ const vcard =
492
+ `BEGIN:VCARD\nVERSION:3.0\nFN:${name}\n` +
493
+ (org ? `ORG:${org}\n` : "") +
494
+ `TEL;type=CELL;type=VOICE;waid=${phone}:+${phone}\nEND:VCARD`;
495
+ return { displayName: name, vcard };
496
+ });
497
+
498
+ return sock.sendMessage(
499
+ jid,
500
+ {
501
+ contacts: {
502
+ displayName: list.length === 1 ? list[0].displayName : `${list.length} contacts`,
503
+ contacts: list,
504
+ },
505
+ },
506
+ options
507
+ );
508
+ }
509
+
510
+ /** product: { title, description, price, currencyCode, productId, retailerId, url, image|thumbnail } (الصورة مطلوبة) */
511
+ export async function sendProduct(sock, jid, product = {}, options = {}) {
512
+ assertSock(sock);
513
+ const productImage = product.image || product.thumbnail || product.productImage;
514
+ if (!productImage) throw new Error("sendProduct requires an image (image | thumbnail | productImage)");
515
+ const image = typeof productImage === "string" ? { url: productImage } : productImage;
516
+
517
+ return sock.sendMessage(
518
+ jid,
519
+ {
520
+ product: {
521
+ productId: String(product.productId || "1"),
522
+ title: product.title || "Product",
523
+ description: product.description || "",
524
+ currencyCode: product.currencyCode || "USD",
525
+ priceAmount1000: product.price ? Math.round(Number(product.price) * 1000) : 0,
526
+ retailerId: product.retailerId || "prototypevip",
527
+ url: product.url || "",
528
+ productImageCount: 1,
529
+ productImage: image,
530
+ },
531
+ businessOwnerJid: product.businessOwnerJid || sock.user?.id,
532
+ body: product.body || product.description || "",
533
+ footer: product.footer || "",
534
+ },
535
+ options
536
+ );
537
+ }
538
+
539
+ /** sticker: Buffer | { url } | رابط/مسار نصي */
540
+ export async function sendSticker(sock, jid, sticker, options = {}) {
541
+ assertSock(sock);
542
+ const media = typeof sticker === "string" ? { url: sticker } : sticker;
543
+ return sock.sendMessage(jid, { sticker: media }, options);
544
+ }
545
+
546
+ /* ───────────────────────────── calls ───────────────────────────── */
547
+
548
+ /**
549
+ * مكالمة مجدولة (Event بنوع call). مفيش outbound WebRTC call حقيقي في Baileys.
550
+ * callType: "audio" | "video"
551
+ */
552
+ export async function sendScheduledCall(sock, jid, content = {}, options = {}) {
553
+ assertSock(sock);
554
+ const {
555
+ name = "Call",
556
+ description = "",
557
+ startDate = new Date(Date.now() + 5 * 60 * 1000),
558
+ endDate,
559
+ callType = "audio",
560
+ extraGuestsAllowed = true,
561
+ location,
562
+ } = content;
563
+
564
+ const toDate = (d) => (d instanceof Date ? d : new Date(d));
565
+ const start = toDate(startDate);
566
+ if (Number.isNaN(start.getTime())) throw new Error("sendScheduledCall: invalid startDate");
567
+ const end = endDate ? toDate(endDate) : new Date(start.getTime() + 60 * 60 * 1000);
568
+ if (Number.isNaN(end.getTime())) throw new Error("sendScheduledCall: invalid endDate");
569
+
570
+ return sock.sendMessage(
571
+ jid,
572
+ {
573
+ event: {
574
+ name,
575
+ description,
576
+ startDate: start,
577
+ endDate: end,
578
+ call: callType === "video" ? "video" : "audio",
579
+ isScheduleCall: true,
580
+ extraGuestsAllowed,
581
+ location,
582
+ },
583
+ },
584
+ options
585
+ );
586
+ }
587
+
588
+ export async function sendCallInvite(sock, jid, content = {}, options = {}) {
589
+ const {
590
+ name = "مكالمة واردة",
591
+ description = "اضغط للانضمام للمكالمة",
592
+ callType = "audio",
593
+ minutesFromNow = 2,
594
+ } = typeof content === "string" ? { name: content } : content;
595
+
596
+ return sendScheduledCall(
597
+ sock,
598
+ jid,
599
+ {
600
+ name,
601
+ description,
602
+ startDate: new Date(Date.now() + minutesFromNow * 60 * 1000),
603
+ callType,
604
+ extraGuestsAllowed: true,
605
+ },
606
+ options
607
+ );
608
+ }
609
+
610
+ /* ───────────────────────────── JID / LID ───────────────────────────── */
611
+
612
+ export function isLid(jid) {
613
+ return typeof jid === "string" && jid.endsWith("@lid");
614
+ }
615
+
616
+ export function isPn(jid) {
617
+ return typeof jid === "string" && jid.endsWith("@s.whatsapp.net");
618
+ }
619
+
620
+ const stripDevice = (jid) => {
621
+ if (typeof jid !== "string") return jid;
622
+ const [user, server] = jid.split("@");
623
+ return `${user.split(":")[0]}@${server}`;
624
+ };
625
+
626
+ /**
627
+ * LID مش مشتق من رقم التليفون — لازم mapping حقيقي من WhatsApp.
628
+ * بتستعمل sock.signalRepository.lidMapping. بترجّع null لو الـ mapping مش معروف.
629
+ */
630
+ export async function toLid(sock, jid) {
631
+ if (!jid) return jid;
632
+ if (isLid(jid)) return stripDevice(jid);
633
+ const lid = await sock?.signalRepository?.lidMapping?.getLIDForPN?.(stripDevice(jid));
634
+ return lid ? stripDevice(lid) : null;
635
+ }
636
+
637
+ export async function toPn(sock, jid) {
638
+ if (!jid) return jid;
639
+ if (isPn(jid)) return stripDevice(jid);
640
+ const pn = await sock?.signalRepository?.lidMapping?.getPNForLID?.(stripDevice(jid));
641
+ return pn ? stripDevice(pn) : null;
642
+ }
643
+
644
+ /** يشيل رقم الجهاز (:12) من أي JID من غير ما يغيّر نوعه */
645
+ export function normalizeJid(jid) {
646
+ return stripDevice(jid);
647
+ }
648
+
649
+ /* ───────────────────────────── attach ───────────────────────────── */
650
+
651
+ /**
652
+ * يربط كل الـ helpers على sock:
653
+ * attachPrototypeHelpers(sock); await sock.sendButtons(jid, {...})
654
+ */
655
+ export function attachPrototypeHelpers(sock) {
656
+ if (!sock || typeof sock.relayMessage !== "function" || typeof sock.sendMessage !== "function") {
657
+ throw new Error("Invalid socket instance");
658
+ }
659
+
660
+ sock.sendButtons = (jid, content, opts) => sendButtons(sock, jid, content, opts);
661
+ sock.sendList = (jid, content, opts) => sendList(sock, jid, content, opts);
662
+ sock.sendAlbum = (jid, media, opts) => sendAlbum(sock, jid, media, opts);
663
+ sock.sendRichHtml = (jid, content, opts) => sendRichHtml(sock, jid, content, opts);
664
+ sock.sendRichHtmlAdvanced = (jid, content, opts) => sendRichHtmlAdvanced(sock, jid, content, opts);
665
+ sock.sendRichCode = (jid, content, opts) => sendRichCode(sock, jid, content, opts);
666
+ sock.sendAIMessage = (jid, content, opts) => sendAIMessage(sock, jid, content, opts);
667
+ sock.sendQuickButtons = (jid, text, labels, opts) => sendQuickButtons(sock, jid, text, labels, opts);
668
+ sock.sendCarousel = (jid, content, opts) => sendCarousel(sock, jid, content, opts);
669
+ sock.sendViewOnce = (jid, content, opts) => sendViewOnce(sock, jid, content, opts);
670
+ sock.sendDocument = (jid, content, opts) => sendDocument(sock, jid, content, opts);
671
+ sock.sendEdit = (jid, key, content, opts) => sendEdit(sock, jid, key, content, opts);
672
+ sock.sendDelete = (jid, key) => sendDelete(sock, jid, key);
673
+ sock.sendWithTyping = (jid, content, opts) => sendWithTyping(sock, jid, content, opts);
674
+ sock.sendReaction = (jid, key, emoji) => sendReaction(sock, jid, key, emoji);
675
+ sock.sendPoll = (jid, name, values, opts) => sendPoll(sock, jid, name, values, opts);
676
+ sock.sendMentionAll = (jid, text, opts) => sendMentionAll(sock, jid, text, opts);
677
+ sock.sendLocation = (jid, lat, lng, opts) => sendLocation(sock, jid, lat, lng, opts);
678
+ sock.sendContact = (jid, contacts, opts) => sendContact(sock, jid, contacts, opts);
679
+ sock.sendProduct = (jid, product, opts) => sendProduct(sock, jid, product, opts);
680
+ sock.sendSticker = (jid, sticker, opts) => sendSticker(sock, jid, sticker, opts);
681
+ sock.sendScheduledCall = (jid, content, opts) => sendScheduledCall(sock, jid, content, opts);
682
+ sock.sendCallInvite = (jid, content, opts) => sendCallInvite(sock, jid, content, opts);
683
+
684
+ // JID / LID
685
+ sock.toLid = (jid) => toLid(sock, jid); // async
686
+ sock.toPn = (jid) => toPn(sock, jid); // async
687
+ sock.isLid = isLid;
688
+ sock.isPn = isPn;
689
+ sock.normalizeJid = normalizeJid;
690
+
691
+ // Builders (من message-builder.js)
692
+ sock.button = () => new Button(sock);
693
+ sock.buttonV2 = () => new ButtonV2(sock);
694
+ sock.carouselBuilder = () => new Carousel(sock);
695
+ sock.aiRich = () => new AIRich(sock);
696
+ sock.Toolkit = Toolkit;
697
+
698
+ return sock;
699
+ }
700
+
701
+ export default {
702
+ buildNativeFlowButtons,
703
+ sendButtons,
704
+ sendList,
705
+ sendAlbum,
706
+ sendRichHtml,
707
+ sendRichHtmlAdvanced,
708
+ sendRichCode,
709
+ sendAIMessage,
710
+ sendQuickButtons,
711
+ sendCarousel,
712
+ sendViewOnce,
713
+ sendDocument,
714
+ sendEdit,
715
+ sendDelete,
716
+ sendWithTyping,
717
+ sendReaction,
718
+ sendPoll,
719
+ sendMentionAll,
720
+ sendLocation,
721
+ sendContact,
722
+ sendProduct,
723
+ sendSticker,
724
+ sendScheduledCall,
725
+ sendCallInvite,
726
+ toLid,
727
+ toPn,
728
+ isLid,
729
+ isPn,
730
+ normalizeJid,
731
+ attachPrototypeHelpers,
732
+ };