aunubaileys 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -60,6 +60,19 @@ Everything else is pure upstream. When upstream moves, we rebase the patch layer
60
60
 
61
61
 
62
62
 
63
+
64
+
65
+ ### 🆕 What's new in 1.0.5 / Baru di 1.0.5
66
+
67
+ - 🛡️ **AunuAntiBan built in** — account-protection layer inspired by [baileys-antiban](https://github.com/kobie3717/baileys-antiban), reimplemented cleanly in-package: per-chat & global rate limiting with Gaussian jitter, 7-day warmup ramp for new accounts, group add/create guards, typing simulation, and `classifyDisconnect()` recovery advice. No telemetry, no extra network calls. Usage: `wrapAntiBan(sock)` or `new AunuAntiBan(opts).wrap(sock)`.
68
+
69
+ ### 🆕 What's new in 1.0.4 / Baru di 1.0.4
70
+
71
+ - 🎨 **Rich builders** — `AIRich`, `Button`, `ButtonV2`, `Carousel`, `Toolkit` (+ aliases) for Meta AI-style rich responses, native-flow buttons and carousels; `aunuHandler.sendInteractive()` with automatic plain-text fallback on iOS-paired devices.
72
+ - 🖼️ **Profile pictures fixed** — center crop (was top-left) and jimp v1 compatibility restored.
73
+ - 🛡️ **Hardening** — undecodable app-state patches are skipped with a warning instead of killing sync, media downloads honour the message's own host, `getChatId` fails loudly, real `ev.destroy()` cleanup, group presence carries `groupOnlineCount`, `Browsers.android` added.
74
+ - ✅ Already verified in this codebase: PBKDF2 pairing derivation and the anti-spoof protocol-message guard (GHSA-qvv5-jq5g-4cgg class) match the reference implementations.
75
+
63
76
  ### 🆕 What's new in 1.0.3 / Baru di 1.0.3
64
77
 
65
78
  - ⚡ **Modern core** — latest WA version `2.3000.1035194821`, 812 pre-keys, tuned WebSocket (no per-message deflate, 100 MB payload).
@@ -546,7 +546,9 @@ export const makeChatsSocket = (config) => {
546
546
  if (tag === 'presence') {
547
547
  presence = {
548
548
  lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
549
- lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
549
+ lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
550
+ // AunuBaileys: jumlah online grup bila dikirim WhatsApp
551
+ groupOnlineCount: attrs.count ? +attrs.count : undefined
550
552
  };
551
553
  }
552
554
  else if (Array.isArray(content)) {
@@ -281,6 +281,7 @@ export const makeSocket = (config) => {
281
281
  catch { }
282
282
  }
283
283
  ev.removeAllListeners('connection.update');
284
+ ev.destroy?.();
284
285
  };
285
286
  const waitForSocketOpen = async () => {
286
287
  if (ws.isOpen) {
@@ -0,0 +1,21 @@
1
+ import type { WASocket } from '../Types';
2
+ export declare const gaussianJitter: (mean?: number, sigma?: number) => number;
3
+ export declare class RateLimiter {
4
+ constructor(max: number, windowMs: number);
5
+ acquire(key: string): Promise<number>;
6
+ tryAcquire(key: string): boolean;
7
+ }
8
+ export interface DisconnectClassification { category: string; action: 'stop' | 'restart' | 'reconnect' | 'backoff'; detail: string; }
9
+ export declare const classifyDisconnect: (error: unknown) => DisconnectClassification;
10
+ export interface AunuAntiBanOptions {
11
+ perChatPerMinute?: number; globalPerMinute?: number; minDelayMs?: number; jitterMs?: number;
12
+ warmupDays?: number; warmupStart?: number; typingSimulation?: boolean;
13
+ groupAddMax?: number; groupAddWindowMs?: number; groupCreateMax?: number; groupCreateWindowMs?: number;
14
+ }
15
+ export declare class AunuAntiBan {
16
+ constructor(options?: AunuAntiBanOptions);
17
+ warmupFactor(): number;
18
+ effectiveLimit(base: number): number;
19
+ wrap<T extends WASocket>(sock: T): T;
20
+ }
21
+ export declare const wrapAntiBan: (sock: WASocket, options?: AunuAntiBanOptions) => WASocket;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * AunuAntiBan — lapisan perlindungan akun bawaan AunuBaileys.
3
+ * Konsep terinspirasi baileys-antiban (kobie3717), diimplementasikan ulang
4
+ * secara bersih di dalam paket: rate limiting dengan jitter Gaussian,
5
+ * jadwal warmup akun baru, penjaga operasi grup, simulasi mengetik,
6
+ * dan klasifikasi disconnect. Tanpa telemetri, tanpa jaringan tambahan.
7
+ */
8
+ import { Boom } from '@hapi/boom';
9
+ import { DisconnectReason } from '../Types/index.js';
10
+ import { delay } from './generics.js';
11
+
12
+ /** Jitter Gaussian (Box-Muller), hasil dibatasi >= 0. */
13
+ export const gaussianJitter = (mean = 0, sigma = 1) => {
14
+ let u = 0;
15
+ let v = 0;
16
+ while (u === 0)
17
+ u = Math.random();
18
+ while (v === 0)
19
+ v = Math.random();
20
+ const n = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
21
+ return Math.max(0, mean + n * sigma);
22
+ };
23
+
24
+ /** Rate limiter jendela geser per kunci. */
25
+ export class RateLimiter {
26
+ constructor(max, windowMs) {
27
+ this.max = max;
28
+ this.windowMs = windowMs;
29
+ this.hits = new Map();
30
+ }
31
+ /** Tunggu sampai ada slot untuk key; kembalikan waktu tunggu (ms). */
32
+ async acquire(key) {
33
+ const now = Date.now();
34
+ let arr = this.hits.get(key);
35
+ if (!arr) {
36
+ arr = [];
37
+ this.hits.set(key, arr);
38
+ }
39
+ while (arr.length && now - arr[0] >= this.windowMs)
40
+ arr.shift();
41
+ if (arr.length >= this.max) {
42
+ const wait = this.windowMs - (now - arr[0]) + 5;
43
+ await delay(wait);
44
+ return this.acquire(key);
45
+ }
46
+ arr.push(Date.now());
47
+ return 0;
48
+ }
49
+ /** Cek tanpa menunggu; true bila masih boleh. */
50
+ tryAcquire(key) {
51
+ const now = Date.now();
52
+ let arr = this.hits.get(key);
53
+ if (!arr) {
54
+ arr = [];
55
+ this.hits.set(key, arr);
56
+ }
57
+ while (arr.length && now - arr[0] >= this.windowMs)
58
+ arr.shift();
59
+ if (arr.length >= this.max) {
60
+ return false;
61
+ }
62
+ arr.push(now);
63
+ return true;
64
+ }
65
+ }
66
+
67
+ /** Klasifikasi error disconnect -> kategori + tindakan yang disarankan. */
68
+ export const classifyDisconnect = (error) => {
69
+ const code = error?.output?.statusCode ?? error?.statusCode ?? error;
70
+ switch (code) {
71
+ case DisconnectReason.loggedOut:
72
+ return { category: 'logged_out', action: 'stop', detail: 'Sesi logout — perlu pairing ulang. Jangan auto-reconnect.' };
73
+ case DisconnectReason.restartRequired:
74
+ return { category: 'restart_required', action: 'restart', detail: 'Server meminta restart — hubungkan ulang sekali.' };
75
+ case DisconnectReason.connectionReplaced:
76
+ return { category: 'replaced', action: 'stop', detail: 'Sesi digantikan koneksi lain — periksa perangkat tertaut.' };
77
+ case DisconnectReason.badSession:
78
+ return { category: 'bad_session', action: 'stop', detail: 'Sesi rusak — hapus auth state dan pairing ulang.' };
79
+ case DisconnectReason.multideviceMismatch:
80
+ return { category: 'mismatch', action: 'stop', detail: 'Ketidakcocokan multidevice — pairing ulang.' };
81
+ case 403:
82
+ return { category: 'forbidden', action: 'stop', detail: '403 — kemungkinan dibatasi/diblokir. Berhenti dan periksa akun.' };
83
+ case 429:
84
+ return { category: 'rate_limited', action: 'backoff', detail: '429 — terlalu cepat. Mundur dan turunkan laju kirim.' };
85
+ case DisconnectReason.timedOut:
86
+ case DisconnectReason.connectionLost:
87
+ case DisconnectReason.connectionClosed:
88
+ default:
89
+ return { category: 'transient', action: 'reconnect', detail: 'Gangguan jaringan/server sementara — aman reconnect dengan jeda.' };
90
+ }
91
+ };
92
+
93
+ export class AunuAntiBan {
94
+ constructor(options = {}) {
95
+ this.options = {
96
+ perChatPerMinute: 12,
97
+ globalPerMinute: 60,
98
+ minDelayMs: 700,
99
+ jitterMs: 900,
100
+ warmupDays: 7,
101
+ warmupStart: Date.now(),
102
+ typingSimulation: true,
103
+ groupAddMax: 3,
104
+ groupAddWindowMs: 600000,
105
+ groupCreateMax: 2,
106
+ groupCreateWindowMs: 600000,
107
+ ...options
108
+ };
109
+ const o = this.options;
110
+ this.perChat = new RateLimiter(o.perChatPerMinute, 60000);
111
+ this.global = new RateLimiter(o.globalPerMinute, 60000);
112
+ this.groupAdd = new RateLimiter(o.groupAddMax, o.groupAddWindowMs);
113
+ this.groupCreate = new RateLimiter(o.groupCreateMax, o.groupCreateWindowMs);
114
+ }
115
+ /** Faktor warmup 0.3 -> 1.0 selama warmupDays sejak warmupStart. */
116
+ warmupFactor() {
117
+ const days = (Date.now() - this.options.warmupStart) / 86400000;
118
+ if (days >= this.options.warmupDays) {
119
+ return 1;
120
+ }
121
+ return 0.3 + 0.7 * Math.max(0, days / this.options.warmupDays);
122
+ }
123
+ /** Batasi laju efektif mengikuti warmup. */
124
+ effectiveLimit(base) {
125
+ return Math.max(1, Math.floor(base * this.warmupFactor()));
126
+ }
127
+ /** Terapkan perlindungan ke socket; socket yang sama dikembalikan. */
128
+ wrap(sock) {
129
+ const self = this;
130
+ const origSend = sock.sendMessage.bind(sock);
131
+ sock.sendMessage = async (jid, content, options) => {
132
+ await self.global.acquire('__global__');
133
+ await self.perChat.acquire(jid);
134
+ if (self.options.typingSimulation && (content?.text || content?.caption) && typeof sock.sendPresenceUpdate === 'function') {
135
+ try {
136
+ await sock.sendPresenceUpdate('composing', jid);
137
+ }
138
+ catch { }
139
+ }
140
+ const wait = self.options.minDelayMs * self.warmupFactor() + gaussianJitter(0, self.options.jitterMs / 2);
141
+ if (wait > 0) {
142
+ await delay(wait);
143
+ }
144
+ return origSend(jid, content, options);
145
+ };
146
+ if (typeof sock.groupParticipantsUpdate === 'function') {
147
+ const orig = sock.groupParticipantsUpdate.bind(sock);
148
+ sock.groupParticipantsUpdate = async (jid, participants, action, inviteMessage) => {
149
+ if (action === 'add' && !self.groupAdd.tryAcquire(jid)) {
150
+ throw new Boom('AunuAntiBan: batas tambah anggota grup tercapai — jeda dulu agar akun aman', { statusCode: 429 });
151
+ }
152
+ return orig(jid, participants, action, inviteMessage);
153
+ };
154
+ }
155
+ if (typeof sock.groupCreate === 'function') {
156
+ const origCreate = sock.groupCreate.bind(sock);
157
+ sock.groupCreate = async (subject, participants) => {
158
+ if (!self.groupCreate.tryAcquire('__create__')) {
159
+ throw new Boom('AunuAntiBan: batas buat grup tercapai — jeda dulu agar akun aman', { statusCode: 429 });
160
+ }
161
+ return origCreate(subject, participants);
162
+ };
163
+ }
164
+ sock.aunuAntiBan = self;
165
+ return sock;
166
+ }
167
+ }
168
+
169
+ /** Bungkus socket dengan AunuAntiBan (jalan pintas). */
170
+ export const wrapAntiBan = (sock, options) => new AunuAntiBan(options).wrap(sock);
@@ -20,6 +20,23 @@ export class AunuHandler {
20
20
  async react(jid, emoji, key) {
21
21
  return this.sock.sendMessage(jid, { react: { text: emoji, key } });
22
22
  }
23
+ /**
24
+ * Kirim pesan interaktif; otomatis fallback ke teks polos bila
25
+ * perangkat pasangan adalah iOS (tombol tidak tampil andal di sana)
26
+ * atau bila options.forceFallback diaktifkan.
27
+ */
28
+ async sendInteractive(jid, content, options = {}) {
29
+ const platform = this.sock.authState?.creds?.platform;
30
+ if (options.forceFallback || platform === 'ios') {
31
+ const i = content?.interactive || {};
32
+ const text = options.fallbackText || i.text || i.body?.text || '';
33
+ const rest = { ...options };
34
+ delete rest.forceFallback;
35
+ delete rest.fallbackText;
36
+ return this.sock.sendMessage(jid, { text }, rest);
37
+ }
38
+ return this.sock.sendMessage(jid, content, options);
39
+ }
23
40
  /** ID numerik dari JID newsletter. */
24
41
  newsletterId(jid) {
25
42
  return this.sock.newsletterId(jid);
@@ -0,0 +1,31 @@
1
+ import type { AnyMessageContent } from '../Types';
2
+ export declare class Button {
3
+ title(t: string): this; body(t: string): this; footer(t: string): this;
4
+ reply(id: string, text: string): this; url(text: string, url: string): this;
5
+ copy(text: string, code: string): this; call(text: string, phoneNumber: string): this;
6
+ build(): AnyMessageContent;
7
+ }
8
+ export declare class ButtonV2 {
9
+ body(t: string): this; footer(t: string): this;
10
+ list(buttonText: string, sections: unknown[]): this;
11
+ reply(id: string, text: string): this; url(text: string, url: string): this;
12
+ build(): AnyMessageContent;
13
+ }
14
+ export declare class Carousel {
15
+ body(t: string): this; card(card: Record<string, unknown>): this;
16
+ build(): AnyMessageContent;
17
+ }
18
+ export declare class AIRich {
19
+ text(t: string): this; code(code: string, language?: string): this;
20
+ latex(t: string): this; table(table: unknown): this; raw(sub: unknown): this;
21
+ build(): AnyMessageContent;
22
+ }
23
+ export declare class Toolkit {
24
+ static quickReply(id: string, text: string): unknown;
25
+ static urlButton(text: string, url: string): unknown;
26
+ static copyButton(text: string, code: string): unknown;
27
+ static callButton(text: string, phoneNumber: string): unknown;
28
+ static listSection(title: string, rows: { id: string; title: string; description?: string }[]): unknown;
29
+ static listButton(text: string, title: string, rows: { id: string; title: string; description?: string }[]): unknown;
30
+ }
31
+ export { Button as Buttons, Button as ButtonBuilder, ButtonV2 as ListButton, Carousel as CarouselBuilder, AIRich as AiRich, AIRich as RichResponse, Toolkit as Tools };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Rich message builders khas AunuBaileys — API chainable untuk tombol,
3
+ * carousel, dan respons kaya ala Meta AI. Hasil build() adalah
4
+ * AnyMessageContent yang siap dikirim via sock.sendMessage().
5
+ */
6
+ const nf = (name, params) => ({ name, buttonParamsJson: JSON.stringify(params) });
7
+
8
+ /** Builder tombol interaktif (native flow). Alias: Buttons, ButtonBuilder. */
9
+ export class Button {
10
+ constructor() {
11
+ this._text = '';
12
+ this._footer = '';
13
+ this._title = '';
14
+ this._buttons = [];
15
+ }
16
+ title(t) { this._title = t; return this; }
17
+ body(t) { this._text = t; return this; }
18
+ footer(t) { this._footer = t; return this; }
19
+ /** Tombol balasan cepat. */
20
+ reply(id, text) { this._buttons.push(nf('quick_reply', { display_text: text, id: String(id) })); return this; }
21
+ /** Tombol buka tautan. */
22
+ url(text, url) { this._buttons.push(nf('cta_url', { display_text: text, url, merchant_url: url })); return this; }
23
+ /** Tombol salin teks/kode. */
24
+ copy(text, code) { this._buttons.push(nf('cta_copy', { display_text: text, copy_code: code, id: 'copy' })); return this; }
25
+ /** Tombol telepon. */
26
+ call(text, phoneNumber) { this._buttons.push(nf('cta_call', { display_text: text, phone_number: phoneNumber })); return this; }
27
+ build() {
28
+ return {
29
+ interactive: {
30
+ text: this._text,
31
+ footerText: this._footer || undefined,
32
+ header: this._title ? { title: this._title, hasMediaAttachment: false } : undefined,
33
+ buttons: this._buttons
34
+ }
35
+ };
36
+ }
37
+ }
38
+
39
+ /** Builder daftar pilihan (single_select) + tombol lanjutan. Alias: ListButton. */
40
+ export class ButtonV2 {
41
+ constructor() {
42
+ this._text = '';
43
+ this._footer = '';
44
+ this._buttons = [];
45
+ this._sections = [];
46
+ }
47
+ body(t) { this._text = t; return this; }
48
+ footer(t) { this._footer = t; return this; }
49
+ /** Satu tombol daftar berisi sections/rows. */
50
+ list(buttonText, sections) {
51
+ this._buttons.push(nf('single_select', { title: buttonText, sections }));
52
+ return this;
53
+ }
54
+ reply(id, text) { this._buttons.push(nf('quick_reply', { display_text: text, id: String(id) })); return this; }
55
+ url(text, url) { this._buttons.push(nf('cta_url', { display_text: text, url, merchant_url: url })); return this; }
56
+ build() {
57
+ return { interactive: { text: this._text, footerText: this._footer || undefined, buttons: this._buttons } };
58
+ }
59
+ }
60
+
61
+ /** Builder carousel kartu geser. Alias: CarouselBuilder. */
62
+ export class Carousel {
63
+ constructor() {
64
+ this._text = '';
65
+ this._cards = [];
66
+ }
67
+ body(t) { this._text = t; return this; }
68
+ /** card: { title, body, footer?, imageMessage?, buttons?: Button instance | array nativeFlow } */
69
+ card(card) {
70
+ let buttons = card.buttons || [];
71
+ if (buttons instanceof Button) {
72
+ buttons = buttons.build().interactive.buttons;
73
+ }
74
+ this._cards.push({ title: card.title, body: card.body || '', footer: card.footer, imageMessage: card.imageMessage, buttons });
75
+ return this;
76
+ }
77
+ build() {
78
+ return { interactive: { text: this._text, carousel: this._cards } };
79
+ }
80
+ }
81
+
82
+ /** Builder respons kaya ala Meta AI (AIRichResponseMessage). Alias: AiRich, RichResponse. */
83
+ export class AIRich {
84
+ constructor() {
85
+ this._subs = [];
86
+ }
87
+ text(t) { this._subs.push({ type: 'text', text: t }); return this; }
88
+ code(code, language = 'text') { this._subs.push({ type: 'code', code, language }); return this; }
89
+ latex(t) { this._subs.push({ type: 'latex', text: t }); return this; }
90
+ /** table: proto tableMetadata mentah { title, rows: [...] } */
91
+ table(table) { this._subs.push({ type: 'table', table }); return this; }
92
+ raw(sub) { this._subs.push({ raw: sub }); return this; }
93
+ build() {
94
+ return { aiRich: { submessages: this._subs } };
95
+ }
96
+ }
97
+
98
+ /** Helper statis campuran. Alias: Tools. */
99
+ export class Toolkit {
100
+ static quickReply(id, text) { return nf('quick_reply', { display_text: text, id: String(id) }); }
101
+ static urlButton(text, url) { return nf('cta_url', { display_text: text, url, merchant_url: url }); }
102
+ static copyButton(text, code) { return nf('cta_copy', { display_text: text, copy_code: code, id: 'copy' }); }
103
+ static callButton(text, phoneNumber) { return nf('cta_call', { display_text: text, phone_number: phoneNumber }); }
104
+ static listSection(title, rows) {
105
+ return { title, rows: rows.map(r => ({ id: String(r.id), title: r.title, description: r.description || '' })) };
106
+ }
107
+ /** Ubah daftar {id, title, description} menjadi satu tombol single_select. */
108
+ static listButton(text, title, rows) {
109
+ return nf('single_select', { title: text, sections: [Toolkit.listSection(title, rows)] });
110
+ }
111
+ }
112
+ export const Buttons = Button;
113
+ export const ButtonBuilder = Button;
114
+ export const ListButton = ButtonV2;
115
+ export const CarouselBuilder = Carousel;
116
+ export const AiRich = AIRich;
117
+ export const RichResponse = AIRich;
118
+ export const Tools = Toolkit;
@@ -286,6 +286,7 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
286
286
  };
287
287
  const mutationMap = {};
288
288
  for (const syncd of syncds) {
289
+ try {
289
290
  const { version, keyId, snapshotMac } = syncd;
290
291
  if (syncd.externalMutations) {
291
292
  logger?.trace({ name, version }, 'downloading external patch');
@@ -318,6 +319,12 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
318
319
  }
319
320
  // clear memory used up by the mutations
320
321
  syncd.mutations = [];
322
+ }
323
+ catch (err) {
324
+ // AunuBaileys: satu patch rusak tidak membunuh seluruh sinkronisasi
325
+ logger?.warn({ err, name, version: syncd.version }, 'skipping undecodable app state patch');
326
+ syncd.mutations = [];
327
+ }
321
328
  }
322
329
  return { state: newState, mutationMap };
323
330
  };
@@ -100,7 +100,21 @@ export const makeEventBuffer = (logger) => {
100
100
  },
101
101
  on: (...args) => ev.on(...args),
102
102
  off: (...args) => ev.off(...args),
103
- removeAllListeners: (...args) => ev.removeAllListeners(...args)
103
+ removeAllListeners: (...args) => ev.removeAllListeners(...args),
104
+ // AunuBaileys: destroy sungguhan — bersihkan buffer, cache & listener
105
+ destroy() {
106
+ try {
107
+ const fresh = makeBufferData();
108
+ for (const key of Object.keys(data)) {
109
+ delete data[key];
110
+ }
111
+ Object.assign(data, fresh);
112
+ }
113
+ catch { }
114
+ historyCache.clear();
115
+ isBuffering = false;
116
+ ev.removeAllListeners();
117
+ }
104
118
  };
105
119
  };
106
120
  const makeBufferData = () => {
@@ -25,6 +25,8 @@ export const Browsers = {
25
25
  macOS: browser => ['Mac OS', browser, '14.4.1'],
26
26
  baileys: browser => ['Baileys', browser, '6.5.0'],
27
27
  windows: browser => ['Windows', browser, '10.0.22631'],
28
+ /** AunuBaileys: sesi yang tampil sebagai Android */
29
+ android: browser => ['Android', browser, '14'],
28
30
  /** The appropriate browser based on your OS & release */
29
31
  appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
30
32
  };
@@ -114,7 +114,8 @@ export const extractImageThumb = async (bufferOrFilePath, width = 32) => {
114
114
  }
115
115
  };
116
116
  }
117
- else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
117
+ else if ('jimp' in lib && (typeof lib.jimp?.Jimp === 'object' || typeof lib.jimp?.Jimp === 'function')) {
118
+ // AunuBaileys: jimp v1 mengekspos Jimp sebagai class (function), bukan object
118
119
  const jimp = await lib.jimp.Jimp.read(bufferOrFilePath);
119
120
  const dimensions = {
120
121
  width: jimp.width,
@@ -156,10 +157,12 @@ export const generateProfilePicture = async (mediaUpload, dimensions) => {
156
157
  })
157
158
  .toBuffer();
158
159
  }
159
- else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
160
+ else if ('jimp' in lib && (typeof lib.jimp?.Jimp === 'object' || typeof lib.jimp?.Jimp === 'function')) {
161
+ // AunuBaileys: jimp v1 mengekspos Jimp sebagai class (function), bukan object
160
162
  const jimp = await lib.jimp.Jimp.read(buffer);
161
163
  const min = Math.min(jimp.width, jimp.height);
162
- const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min });
164
+ // AunuBaileys: crop dari TENGAH (sebelumnya pojok kiri-atas memotong subjek)
165
+ const cropped = jimp.crop({ x: Math.floor((jimp.width - min) / 2), y: Math.floor((jimp.height - min) / 2), w: min, h: min });
163
166
  img = cropped.resize({ w, h, mode: lib.jimp.ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 });
164
167
  }
165
168
  else {
@@ -386,8 +389,8 @@ const toSmallestChunkSize = (num) => {
386
389
  };
387
390
  export const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;
388
391
  export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
389
- const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/');
390
- const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath);
392
+ // AunuBaileys: pakai host dari URL pesan bila ada (bukan selalu mmg.whatsapp.net)
393
+ const downloadUrl = url?.startsWith('https://') ? url : getUrlFromDirectPath(directPath);
391
394
  if (!downloadUrl) {
392
395
  throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 });
393
396
  }
@@ -643,6 +643,31 @@ export const generateWAMessageContent = async (message, options) => {
643
643
  }
644
644
  }
645
645
  }
646
+ else if ('aiRich' in message) {
647
+ // AunuBaileys: Meta AI-style rich response (AIRichResponseMessage)
648
+ const subs = (message.aiRich.submessages || []).map((s) => {
649
+ if (s.type === 'text' || (!s.type && s.text)) {
650
+ return { messageText: { text: s.text || '' } };
651
+ }
652
+ if (s.type === 'code') {
653
+ return { codeMetadata: { codeContent: s.code || s.text || '', codeLanguage: s.language || 'text' } };
654
+ }
655
+ if (s.type === 'latex') {
656
+ return { latexMetadata: { text: s.text || '' } };
657
+ }
658
+ if (s.type === 'table') {
659
+ return { tableMetadata: s.table };
660
+ }
661
+ return s.raw || s;
662
+ });
663
+ m.richResponseMessage = {
664
+ messageType: proto.AIRichResponseMessage.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
665
+ submessages: subs
666
+ };
667
+ if ('text' in message) {
668
+ // teks pendamping opsional diabaikan oleh proto bila rich response ada
669
+ }
670
+ }
646
671
  else if ('pollResult' in message) {
647
672
  // AunuBaileys: poll result snapshot (dari @itsliaaa/baileys)
648
673
  const pollResultSnapshotMessage = {
@@ -707,6 +732,40 @@ export const generateWAMessageContent = async (message, options) => {
707
732
  else if ('requestPhoneNumber' in message) {
708
733
  m.requestPhoneNumberMessage = {};
709
734
  }
735
+ else if ('interactive' in message) {
736
+ // AunuBaileys: interactive / native-flow (pola @itsliaaa/baileys, disederhanakan)
737
+ const iMsg = message.interactive;
738
+ const interactiveMessage = {
739
+ body: iMsg.body ?? (iMsg.text ? { text: iMsg.text } : undefined),
740
+ footer: iMsg.footer ?? (iMsg.footerText ? { text: iMsg.footerText } : undefined),
741
+ header: iMsg.header,
742
+ contextInfo: iMsg.contextInfo
743
+ };
744
+ if (iMsg.carousel) {
745
+ interactiveMessage.carouselMessage = {
746
+ cards: iMsg.carousel.map(card => ({
747
+ header: card.header ?? (card.title ? { title: card.title, hasMediaAttachment: !!card.imageMessage, imageMessage: card.imageMessage } : undefined),
748
+ body: { text: card.body || '' },
749
+ footer: card.footer ? { text: card.footer } : undefined,
750
+ nativeFlowMessage: { buttons: (card.buttons || []).map(b => ({ name: b.name, buttonParamsJson: typeof b.buttonParamsJson === 'string' ? b.buttonParamsJson : JSON.stringify(b.buttonParamsJson ?? {}) })) }
751
+ })),
752
+ messageVersion: 1
753
+ };
754
+ }
755
+ else {
756
+ interactiveMessage.nativeFlowMessage = {
757
+ buttons: (iMsg.buttons || []).map(b => ({
758
+ name: b.name,
759
+ buttonParamsJson: typeof b.buttonParamsJson === 'string' ? b.buttonParamsJson : JSON.stringify(b.buttonParamsJson ?? {})
760
+ })),
761
+ messageParamsJson: iMsg.messageParamsJson || ''
762
+ };
763
+ }
764
+ m = {
765
+ interactiveMessage,
766
+ messageContextInfo: { deviceListMetadata: {}, deviceListMetadataVersion: 2 }
767
+ };
768
+ }
710
769
  else if ('album' in message) {
711
770
  // AunuBaileys: album message (dari @itsliaaa/baileys)
712
771
  if (!Array.isArray(message.album)) {
@@ -1,3 +1,4 @@
1
+ import Boom from '@hapi/boom';
1
2
  import { proto } from '../../WAProto/index.js';
2
3
  import { WAMessageStubType } from '../Types/index.js';
3
4
  import { getContentType, normalizeMessageContent } from '../Utils/messages.js';
@@ -61,10 +62,12 @@ export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !me
61
62
  * Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
62
63
  */
63
64
  export const getChatId = ({ remoteJid, participant, fromMe }) => {
64
- if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
65
- return participant;
65
+ // AunuBaileys: gagal dengan jelas, jangan diam-diam memakai undefined
66
+ const id = isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe ? participant : remoteJid;
67
+ if (!id) {
68
+ throw new Boom('getChatId: missing remoteJid/participant in message key', { statusCode: 400 });
66
69
  }
67
- return remoteJid;
70
+ return id;
68
71
  };
69
72
  /**
70
73
  * Decrypt a poll vote
package/lib/index.d.ts CHANGED
@@ -14,3 +14,5 @@ export * from './Store/index.js';
14
14
  export { processedMessages, initInMemoryKeyStore } from './Utils/fork-compat.js';
15
15
  export { PHONENUMBER_MCC } from './Utils/phonenumber-mcc.js';
16
16
  export * from './Utils/aunu-handler';
17
+ export * from './Utils/builders';
18
+ export * from './Utils/antiban';
package/lib/index.js CHANGED
@@ -13,3 +13,5 @@ export * from './Store/index.js';
13
13
  export { processedMessages, initInMemoryKeyStore } from './Utils/fork-compat.js';
14
14
  export { PHONENUMBER_MCC } from './Utils/phonenumber-mcc.js';
15
15
  export * from './Utils/aunu-handler.js';
16
+ export * from './Utils/builders.js';
17
+ export * from './Utils/antiban.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aunubaileys",
3
3
  "type": "module",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "description": "AunuBaileys \u2014 a WhatsApp Web API for bots that never sleep. Full upstream power with the beloved in-memory store restored and pairing that just works. Forged by AunuCore.",
6
6
  "keywords": [
7
7
  "whatsapp",