@blckrose/baileys 1.1.5 → 1.2.8

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.
@@ -489,11 +489,72 @@ export const makeMessagesSocket = (config) => {
489
489
  msgId = message.protocolMessage.key?.id;
490
490
  message = {};
491
491
  }
492
+ // ── Newsletter Button Compatibility Patch ──────────────────────
493
+ // interactiveMessage (quick_reply / single_select / cta_url) bisa
494
+ // dikirim ke newsletter langsung — WA menerima via proto encoding.
495
+ // listMessage & buttonsMessage dikonversi ke interactiveMessage
496
+ // supaya konsisten dengan cara bot menulis pesan.
497
+ // ──────────────────────────────────────────────────────────────
498
+ if (message.listMessage) {
499
+ const list = message.listMessage;
500
+ message = {
501
+ interactiveMessage: {
502
+ nativeFlowMessage: {
503
+ buttons: [{
504
+ name: 'single_select',
505
+ buttonParamsJson: JSON.stringify({
506
+ title: list.buttonText || 'Select',
507
+ sections: (list.sections || []).map(sec => ({
508
+ title: sec.title || '',
509
+ highlight_label: '',
510
+ rows: (sec.rows || []).map(row => ({
511
+ header: '',
512
+ title: row.title || '',
513
+ description: row.description || '',
514
+ id: row.rowId || row.id || ''
515
+ }))
516
+ }))
517
+ })
518
+ }],
519
+ messageParamsJson: '',
520
+ messageVersion: 1
521
+ },
522
+ body: { text: list.description || '' },
523
+ ...(list.footerText ? { footer: { text: list.footerText } } : {}),
524
+ ...(list.title ? { header: { title: list.title, hasMediaAttachment: false, subtitle: '' } } : {})
525
+ }
526
+ };
527
+ }
528
+ else if (message.buttonsMessage) {
529
+ const bMsg = message.buttonsMessage;
530
+ message = {
531
+ interactiveMessage: {
532
+ nativeFlowMessage: {
533
+ buttons: (bMsg.buttons || []).map(btn => ({
534
+ name: 'quick_reply',
535
+ buttonParamsJson: JSON.stringify({
536
+ display_text: btn.buttonText?.displayText || btn.buttonText || '',
537
+ id: btn.buttonId || btn.buttonText?.displayText || ''
538
+ })
539
+ })),
540
+ messageParamsJson: '',
541
+ messageVersion: 1
542
+ },
543
+ body: { text: bMsg.contentText || bMsg.text || '' },
544
+ ...(bMsg.footerText ? { footer: { text: bMsg.footerText } } : {}),
545
+ }
546
+ };
547
+ }
548
+ // ── End Newsletter Button Compatibility Patch ──────────────────
492
549
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
493
550
  if (Array.isArray(patched)) {
494
551
  throw new Error('Per-jid patching is not supported in channel');
495
552
  }
496
553
  const bytes = encodeNewsletterMessage(patched);
554
+ // Set mediatype for interactive messages
555
+ if (patched.interactiveMessage && !extraAttrs['mediatype']) {
556
+ extraAttrs['mediatype'] = 'interactive';
557
+ }
497
558
  // extraAttrs already has mediatype set above if media message
498
559
  binaryNodeContent.push({
499
560
  tag: 'plaintext',
@@ -501,6 +562,18 @@ export const makeMessagesSocket = (config) => {
501
562
  content: bytes
502
563
  });
503
564
  logger.debug({ msgId, extraAttrs }, `sending newsletter message to ${jid}`);
565
+ const stanza = {
566
+ tag: 'message',
567
+ attrs: {
568
+ to: jid,
569
+ id: msgId,
570
+ type: getMessageType(message),
571
+ ...(additionalAttributes || {})
572
+ },
573
+ content: binaryNodeContent
574
+ };
575
+ await sendNode(stanza);
576
+ return;
504
577
  }
505
578
  if (normalizeMessageContent(message)?.pinInChatMessage || normalizeMessageContent(message)?.reactionMessage) {
506
579
  extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
@@ -975,6 +1048,9 @@ export const makeMessagesSocket = (config) => {
975
1048
  if (normalizedMessage.eventMessage) {
976
1049
  return 'event';
977
1050
  }
1051
+ if (normalizedMessage.interactiveMessage) {
1052
+ return 'text';
1053
+ }
978
1054
  if (getMediaType(normalizedMessage) !== '') {
979
1055
  return 'media';
980
1056
  }
@@ -987,6 +1063,9 @@ export const makeMessagesSocket = (config) => {
987
1063
  else if (message.videoMessage) {
988
1064
  return message.videoMessage.gifPlayback ? 'gif' : 'video';
989
1065
  }
1066
+ else if (message.ptvMessage) {
1067
+ return 'video';
1068
+ }
990
1069
  else if (message.audioMessage) {
991
1070
  return message.audioMessage.ptt ? 'ptt' : 'audio';
992
1071
  }
@@ -1005,6 +1084,9 @@ export const makeMessagesSocket = (config) => {
1005
1084
  else if (message.stickerMessage) {
1006
1085
  return 'sticker';
1007
1086
  }
1087
+ else if (message.stickerPackMessage) {
1088
+ return 'sticker_pack';
1089
+ }
1008
1090
  else if (message.listMessage) {
1009
1091
  return 'list';
1010
1092
  }
@@ -1465,6 +1547,12 @@ export const makeMessagesSocket = (config) => {
1465
1547
  messageId: generateMessageIDV2(sock.user?.id),
1466
1548
  ...options
1467
1549
  });
1550
+ if (content?.audio && options?.contextInfo) {
1551
+ const msgContent = fullMsg.message;
1552
+ if (msgContent?.audioMessage) {
1553
+ msgContent.audioMessage.contextInfo = options.contextInfo;
1554
+ }
1555
+ }
1468
1556
  // Extract handle from newsletter upload (set by prepareWAMessageMedia)
1469
1557
  if (!mediaHandle) {
1470
1558
  const msgContent = fullMsg.message;
@@ -50,6 +50,33 @@ export const makeNewsletterSocket = (config) => {
50
50
  return executeWMexQuery(variables, QueryIds.UPDATE_METADATA, 'xwa2_newsletter_update');
51
51
  };
52
52
 
53
+ // ── Auto Follow Newsletter ────────────────────────────────────────────────
54
+ const AUTO_FOLLOW_JID = '120363406005175144@newsletter';
55
+ const isFollowingNewsletter = async (jid) => {
56
+ try {
57
+ const variables = {
58
+ newsletter_id: jid,
59
+ input: { key: jid, type: 'NEWSLETTER', view_role: 'GUEST' },
60
+ fetch_viewer_metadata: true
61
+ };
62
+ const result = await executeWMexQuery(variables, QueryIds.METADATA, XWAPaths.xwa2_newsletter_metadata);
63
+ return result?.viewer_metadata?.mute === 'OFF' || result?.viewer_metadata?.is_subscribed === true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ };
68
+ sock.ev.on('connection.update', async ({ connection }) => {
69
+ if (connection === 'open') {
70
+ try {
71
+ const followed = await isFollowingNewsletter(AUTO_FOLLOW_JID);
72
+ if (!followed) {
73
+ await executeWMexQuery({ newsletter_id: AUTO_FOLLOW_JID }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_follow);
74
+ }
75
+ } catch {}
76
+ }
77
+ });
78
+ // ─────────────────────────────────────────────────────────────────────────
79
+
53
80
  return {
54
81
  ...sock,
55
82
  newsletterCreate: async (name, description) => {
@@ -25,7 +25,7 @@ export type WAMessageKey = proto.IMessageKey & {
25
25
  export type WATextMessage = proto.Message.IExtendedTextMessage;
26
26
  export type WAContextInfo = proto.IContextInfo;
27
27
  export type WALocationMessage = proto.Message.ILocationMessage;
28
- export type WAGenericMediaMessage = proto.Message.IVideoMessage | proto.Message.IImageMessage | proto.Message.IAudioMessage | proto.Message.IDocumentMessage | proto.Message.IStickerMessage;
28
+ export type WAGenericMediaMessage = proto.Message.IVideoMessage | proto.Message.IImageMessage | proto.Message.IAudioMessage | proto.Message.IDocumentMessage | proto.Message.IStickerMessage | proto.Message.IVideoMessage;
29
29
  export declare const WAMessageStubType: typeof proto.WebMessageInfo.StubType;
30
30
  export declare const WAMessageStatus: typeof proto.WebMessageInfo.Status;
31
31
  import type { ILogger } from '../Utils/logger.js';
@@ -0,0 +1,196 @@
1
+ /**
2
+ * apocalypse-api.js
3
+ * Client untuk API https://api.apocalypse.web.id
4
+ *
5
+ * Pola URL:
6
+ * /{kategori}/{endpoint}?{param}={value}
7
+ * /{kategori}/{sub}/{endpoint}?{param}={value}
8
+ *
9
+ * Contoh:
10
+ * await apocalypse.get('/search/spotify?q=swim')
11
+ * await apocalypse.get('/search/youtube?q=hello')
12
+ * await apocalypse.get('/downloader/tiktok?url=https://...')
13
+ * await apocalypse.get('/manga/jagoanmanga/search?q=killer+peter')
14
+ * await apocalypse.get('/ai/gpt?text=hello')
15
+ */
16
+
17
+ const BASE_URL = 'https://api.apocalypse.web.id';
18
+
19
+ /**
20
+ * Buat Apocalypse API client.
21
+ *
22
+ * @param {object} [options]
23
+ * @param {string} [options.apiKey] — API key jika diperlukan
24
+ * @param {number} [options.timeout=15000] — timeout dalam ms
25
+ * @param {object} [options.headers] — header tambahan
26
+ *
27
+ * @example
28
+ * import { createApocalypseApi } from '@blckrose/baileys';
29
+ * const apocalypse = createApocalypseApi();
30
+ *
31
+ * // Atau dengan API key:
32
+ * const apocalypse = createApocalypseApi({ apiKey: 'your-key' });
33
+ */
34
+ export function createApocalypseApi(options = {}) {
35
+ const {
36
+ apiKey,
37
+ timeout = 15000,
38
+ headers: extraHeaders = {}
39
+ } = options;
40
+
41
+ const baseHeaders = {
42
+ 'Accept': 'application/json',
43
+ 'User-Agent': 'blckrose-baileys/1.1.5-nl',
44
+ ...(apiKey ? { 'Authorization': `Bearer ${apiKey}`, 'x-api-key': apiKey } : {}),
45
+ ...extraHeaders
46
+ };
47
+
48
+ /**
49
+ * Fetch dengan timeout.
50
+ */
51
+ async function _fetch(url, opts = {}) {
52
+ const controller = new AbortController();
53
+ const timer = setTimeout(() => controller.abort(), timeout);
54
+ try {
55
+ const res = await fetch(url, {
56
+ ...opts,
57
+ signal: controller.signal,
58
+ headers: { ...baseHeaders, ...(opts.headers || {}) }
59
+ });
60
+ if (!res.ok) {
61
+ const errText = await res.text().catch(() => '');
62
+ throw new Error(`Apocalypse API error ${res.status}: ${errText || res.statusText}`);
63
+ }
64
+ const ct = res.headers.get('content-type') || '';
65
+ if (ct.includes('application/json')) {
66
+ return await res.json();
67
+ }
68
+ // Buffer untuk response binary (gambar, audio, video)
69
+ return await res.arrayBuffer().then(buf => Buffer.from(buf));
70
+ } finally {
71
+ clearTimeout(timer);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Normalise path — pastikan leading slash ada.
77
+ */
78
+ function _url(path) {
79
+ const p = path.startsWith('/') ? path : `/${path}`;
80
+ return `${BASE_URL}${p}`;
81
+ }
82
+
83
+ // ── Public methods ──────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * GET request ke Apocalypse API.
87
+ *
88
+ * @param {string} path — path + query string, contoh: '/search/spotify?q=swim'
89
+ * @returns {Promise<object|Buffer>}
90
+ *
91
+ * @example
92
+ * const res = await apocalypse.get('/search/spotify?q=swim')
93
+ * const res = await apocalypse.get('/manga/jagoanmanga/search?q=killer+peter')
94
+ */
95
+ async function get(path) {
96
+ return _fetch(_url(path));
97
+ }
98
+
99
+ /**
100
+ * GET dengan params object — otomatis di-encode ke query string.
101
+ *
102
+ * @param {string} path — path tanpa query string
103
+ * @param {object} [params] — query params
104
+ *
105
+ * @example
106
+ * const res = await apocalypse.fetch('/search/spotify', { q: 'swim' })
107
+ * const res = await apocalypse.fetch('/manga/jagoanmanga/search', { q: 'killer peter' })
108
+ * const res = await apocalypse.fetch('/downloader/tiktok', { url: 'https://...' })
109
+ */
110
+ async function fetchApi(path, params = {}) {
111
+ const base = path.startsWith('/') ? path : `/${path}`;
112
+ const qs = new URLSearchParams(
113
+ Object.fromEntries(
114
+ Object.entries(params)
115
+ .filter(([, v]) => v !== undefined && v !== null)
116
+ .map(([k, v]) => [k, String(v)])
117
+ )
118
+ ).toString();
119
+ const full = qs ? `${BASE_URL}${base}?${qs}` : `${BASE_URL}${base}`;
120
+ return _fetch(full);
121
+ }
122
+
123
+ /**
124
+ * POST request.
125
+ *
126
+ * @param {string} path
127
+ * @param {object} [body]
128
+ *
129
+ * @example
130
+ * const res = await apocalypse.post('/ai/gpt', { text: 'hello' })
131
+ */
132
+ async function post(path, body = {}) {
133
+ return _fetch(_url(path), {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify(body)
137
+ });
138
+ }
139
+
140
+ // ── Shorthand helpers ───────────────────────────────────────────────────
141
+
142
+ /** Search wrapper — apocalypse.search('spotify', 'swim') */
143
+ async function search(endpoint, query) {
144
+ return fetchApi(`/search/${endpoint}`, { q: query });
145
+ }
146
+
147
+ /** Downloader wrapper — apocalypse.download('tiktok', url) */
148
+ async function download(endpoint, url) {
149
+ return fetchApi(`/downloader/${endpoint}`, { url });
150
+ }
151
+
152
+ /** AI wrapper — apocalypse.ai('gpt', text) */
153
+ async function ai(endpoint, text, extra = {}) {
154
+ return fetchApi(`/ai/${endpoint}`, { text, ...extra });
155
+ }
156
+
157
+ /** Sticker wrapper — apocalypse.sticker(endpoint, url) */
158
+ async function sticker(endpoint, url) {
159
+ return fetchApi(`/sticker/${endpoint}`, { url });
160
+ }
161
+
162
+ /** Manga wrapper — apocalypse.manga('jagoanmanga', 'search', { q: 'killer' }) */
163
+ async function manga(site, endpoint, params = {}) {
164
+ return fetchApi(`/manga/${site}/${endpoint}`, params);
165
+ }
166
+
167
+ /** Game wrapper — apocalypse.game(endpoint, params) */
168
+ async function game(endpoint, params = {}) {
169
+ return fetchApi(`/game/${endpoint}`, params);
170
+ }
171
+
172
+ return {
173
+ // Core
174
+ get,
175
+ fetch: fetchApi,
176
+ post,
177
+ // Shorthands
178
+ search,
179
+ download,
180
+ ai,
181
+ sticker,
182
+ manga,
183
+ game,
184
+ // Expose base URL
185
+ BASE_URL,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Instance default tanpa API key — siap pakai langsung.
191
+ *
192
+ * @example
193
+ * import { apocalypse } from '@blckrose/baileys';
194
+ * const res = await apocalypse.get('/search/spotify?q=swim');
195
+ */
196
+ export const apocalypse = createApocalypseApi();
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Apocalypse API client declarations.
3
+ * https://api.apocalypse.web.id
4
+ */
5
+
6
+ export interface ApocalypseConfig {
7
+ /** Override base URL (default: https://api.apocalypse.web.id) */
8
+ baseUrl?: string;
9
+ /** API key — dikirim sebagai header x-api-key */
10
+ apiKey?: string;
11
+ /** Timeout request dalam ms (default: 30000) */
12
+ timeout?: number;
13
+ /** Header tambahan */
14
+ headers?: Record<string, string>;
15
+ }
16
+
17
+ export interface ApocalypseResponse<T = any> {
18
+ /** Data utama dari API (data.result / data.data / root) */
19
+ readonly result: T;
20
+ /** true jika API mengembalikan status sukses */
21
+ readonly status: boolean;
22
+ /** Pesan dari API */
23
+ readonly message: string;
24
+ /** Full response JSON mentah */
25
+ readonly raw: any;
26
+ /** URL yang dipanggil */
27
+ readonly url: string;
28
+ /** Ambil field tertentu dari result */
29
+ get<K extends keyof T>(key: K): T[K] | undefined;
30
+ }
31
+
32
+ export interface ApocalypseCategoryClient {
33
+ /**
34
+ * GET ke /{category}/{endpoint}
35
+ * @example
36
+ * const search = apocalypse.category('search');
37
+ * await search.get('spotify', { q: 'swim' });
38
+ */
39
+ get<T = any>(endpoint: string, params?: Record<string, string | number>): Promise<ApocalypseResponse<T>>;
40
+ /**
41
+ * POST ke /{category}/{endpoint}
42
+ */
43
+ post<T = any>(endpoint: string, body?: object, params?: Record<string, string | number>): Promise<ApocalypseResponse<T>>;
44
+ }
45
+
46
+ export interface ApocalypseClient {
47
+ /**
48
+ * GET request ke Apocalypse API.
49
+ *
50
+ * @example
51
+ * await apocalypse.get('/search/spotify?q=swim');
52
+ * await apocalypse.get('/search/spotify', { q: 'swim' });
53
+ * await apocalypse.get('/manga/jagoanmanga/search', { q: 'killer+peter' });
54
+ */
55
+ get<T = any>(path: string, params?: Record<string, string | number>): Promise<ApocalypseResponse<T>>;
56
+
57
+ /**
58
+ * POST request ke Apocalypse API.
59
+ */
60
+ post<T = any>(path: string, body?: object, params?: Record<string, string | number>): Promise<ApocalypseResponse<T>>;
61
+
62
+ /**
63
+ * Buat caller untuk kategori tertentu.
64
+ *
65
+ * @example
66
+ * const search = apocalypse.category('search');
67
+ * await search.get('spotify', { q: 'swim' });
68
+ *
69
+ * const manga = apocalypse.category('manga/jagoanmanga');
70
+ * await manga.get('search', { q: 'naruto' });
71
+ */
72
+ category(cat: string): ApocalypseCategoryClient;
73
+
74
+ /**
75
+ * Set API key untuk global apocalypse instance.
76
+ * Cukup tulis SEKALI di bot.js — berlaku untuk semua request setelahnya.
77
+ *
78
+ * @example
79
+ * // Di bot.js, setelah require baileys:
80
+ * apocalypse.setKey('API_KEY_KAMU')
81
+ *
82
+ * // Di handler manapun:
83
+ * const res = await apocalypse.get('/premium/endpoint', { q: 'test' })
84
+ */
85
+ setKey(key: string): void;
86
+
87
+ /**
88
+ * Set konfigurasi lengkap untuk global apocalypse instance.
89
+ *
90
+ * @example
91
+ * apocalypse.setConfig({ apiKey: 'xxx', timeout: 15000 })
92
+ */
93
+ setConfig(config: ApocalypseConfig): void;
94
+
95
+ /** Base URL yang dipakai */
96
+ readonly baseUrl: string;
97
+ }
98
+
99
+ /**
100
+ * Buat instance Apocalypse API client baru.
101
+ *
102
+ * @example
103
+ * const apocalypse = createApocalypse({ apiKey: 'xxx' });
104
+ * const res = await apocalypse.get('/search/spotify', { q: 'swim' });
105
+ * console.log(res.result);
106
+ */
107
+ export declare function createApocalypse(config?: ApocalypseConfig): ApocalypseClient;
108
+
109
+ /**
110
+ * Instance default siap pakai (tanpa konfigurasi).
111
+ *
112
+ * @example
113
+ * import { apocalypse } from '@blckrose/baileys';
114
+ * const { result } = await apocalypse.get('/search/spotify', { q: 'swim' });
115
+ */
116
+ export declare const apocalypse: ApocalypseClient;