@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,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Bot Example - WhatsApp bot dengan GPT integration
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Conversational AI dengan OpenAI GPT
|
|
6
|
+
* - Persistent conversation context per user
|
|
7
|
+
* - Image recognition (kirim image, bot describe)
|
|
8
|
+
* - Voice message transcription
|
|
9
|
+
* - Multi-language support
|
|
10
|
+
* - Rate limiting per user (RAM efficient)
|
|
11
|
+
*
|
|
12
|
+
* Setup:
|
|
13
|
+
* 1. npm install openai
|
|
14
|
+
* 2. Set OPENAI_API_KEY environment variable
|
|
15
|
+
* 3. npm start
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* - Kirim pesan text apapun, bot akan reply dengan AI-generated response
|
|
19
|
+
* - !reset untuk reset conversation context
|
|
20
|
+
* - !voice untuk transcribe voice message terakhir
|
|
21
|
+
* - !image untuk generate image (DALL-E)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
makeWASocket,
|
|
26
|
+
useFileAuthState,
|
|
27
|
+
type WAMessage,
|
|
28
|
+
type JIDLike,
|
|
29
|
+
jidToString,
|
|
30
|
+
extractMessageText,
|
|
31
|
+
hasMedia
|
|
32
|
+
} from 'waliwa';
|
|
33
|
+
|
|
34
|
+
// ============== Config ==============
|
|
35
|
+
|
|
36
|
+
const CONFIG = {
|
|
37
|
+
model: process.env.OPENAI_MODEL ?? 'gpt-4o-mini',
|
|
38
|
+
maxTokens: parseInt(process.env.MAX_TOKENS ?? '500', 10),
|
|
39
|
+
temperature: parseFloat(process.env.TEMPERATURE ?? '0.7'),
|
|
40
|
+
systemPrompt: process.env.SYSTEM_PROMPT ?? `You are a helpful WhatsApp assistant.
|
|
41
|
+
- Reply in same language as user
|
|
42
|
+
- Keep responses concise (max 3 paragraphs)
|
|
43
|
+
- Be friendly and helpful
|
|
44
|
+
- If user asks about your identity, say you're an AI assistant on WhatsApp powered by Waliwa`,
|
|
45
|
+
maxHistory: 10, // Max messages per conversation
|
|
46
|
+
rateLimitPerMinute: 20, // Per user
|
|
47
|
+
conversationTimeoutMs: 30 * 60 * 1000, // 30 mins idle = reset context
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ============== Conversation Manager (RAM efficient) ==============
|
|
51
|
+
|
|
52
|
+
interface ConversationMessage {
|
|
53
|
+
role: 'user' | 'assistant' | 'system';
|
|
54
|
+
content: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class ConversationManager {
|
|
59
|
+
private conversations = new Map<string, ConversationMessage[]>();
|
|
60
|
+
private lastActivity = new Map<string, number>();
|
|
61
|
+
private maxHistory: number;
|
|
62
|
+
private timeoutMs: number;
|
|
63
|
+
|
|
64
|
+
constructor(maxHistory: number = 10, timeoutMs: number = 30 * 60 * 1000) {
|
|
65
|
+
this.maxHistory = maxHistory;
|
|
66
|
+
this.timeoutMs = timeoutMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getUserKey(jid: JIDLike): string {
|
|
70
|
+
return jidToString(jid).split(':')[0]; // ignore device suffix
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addMessage(jid: JIDLike, message: ConversationMessage): void {
|
|
74
|
+
const key = this.getUserKey(jid);
|
|
75
|
+
let conv = this.conversations.get(key);
|
|
76
|
+
if (!conv) {
|
|
77
|
+
conv = [];
|
|
78
|
+
this.conversations.set(key, conv);
|
|
79
|
+
}
|
|
80
|
+
conv.push(message);
|
|
81
|
+
// Trim to maxHistory (keep system + last N messages)
|
|
82
|
+
if (conv.length > this.maxHistory) {
|
|
83
|
+
// Keep system prompt (if first) + recent messages
|
|
84
|
+
const systemMsgs = conv.filter(m => m.role === 'system');
|
|
85
|
+
const otherMsgs = conv.filter(m => m.role !== 'system');
|
|
86
|
+
const trimmed = [...systemMsgs.slice(-1), ...otherMsgs.slice(-this.maxHistory + 1)];
|
|
87
|
+
this.conversations.set(key, trimmed);
|
|
88
|
+
}
|
|
89
|
+
this.lastActivity.set(key, Date.now());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getHistory(jid: JIDLike): ConversationMessage[] {
|
|
93
|
+
const key = this.getUserKey(jid);
|
|
94
|
+
this.cleanup(key);
|
|
95
|
+
return this.conversations.get(key) ?? [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
reset(jid: JIDLike): void {
|
|
99
|
+
const key = this.getUserKey(jid);
|
|
100
|
+
this.conversations.delete(key);
|
|
101
|
+
this.lastActivity.delete(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private cleanup(key: string): void {
|
|
105
|
+
const last = this.lastActivity.get(key);
|
|
106
|
+
if (last && Date.now() - last > this.timeoutMs) {
|
|
107
|
+
this.conversations.delete(key);
|
|
108
|
+
this.lastActivity.delete(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Cleanup all expired conversations - call periodically untuk hemat RAM */
|
|
113
|
+
cleanupAll(): void {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
for (const [key, last] of this.lastActivity.entries()) {
|
|
116
|
+
if (now - last > this.timeoutMs) {
|
|
117
|
+
this.conversations.delete(key);
|
|
118
|
+
this.lastActivity.delete(key);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
size(): number {
|
|
124
|
+
return this.conversations.size;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============== Rate Limiter (per user) ==============
|
|
129
|
+
|
|
130
|
+
class UserRateLimiter {
|
|
131
|
+
private counts = new Map<string, { count: number; resetAt: number }>();
|
|
132
|
+
private maxPerMinute: number;
|
|
133
|
+
|
|
134
|
+
constructor(maxPerMinute: number = 20) {
|
|
135
|
+
this.maxPerMinute = maxPerMinute;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
canProceed(userKey: string): boolean {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
let entry = this.counts.get(userKey);
|
|
141
|
+
if (!entry || now > entry.resetAt) {
|
|
142
|
+
entry = { count: 0, resetAt: now + 60000 };
|
|
143
|
+
this.counts.set(userKey, entry);
|
|
144
|
+
}
|
|
145
|
+
if (entry.count >= this.maxPerMinute) return false;
|
|
146
|
+
entry.count++;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
cleanup(): void {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
for (const [key, entry] of this.counts.entries()) {
|
|
153
|
+
if (now > entry.resetAt) {
|
|
154
|
+
this.counts.delete(key);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============== OpenAI Client (lazy loaded) ==============
|
|
161
|
+
|
|
162
|
+
let openaiClient: any = null;
|
|
163
|
+
|
|
164
|
+
async function getOpenAIClient(): Promise<any> {
|
|
165
|
+
if (openaiClient) return openaiClient;
|
|
166
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
167
|
+
if (!apiKey) {
|
|
168
|
+
throw new Error('OPENAI_API_KEY environment variable not set');
|
|
169
|
+
}
|
|
170
|
+
// Dynamic import - avoids dependency jika gak dipakai
|
|
171
|
+
const { default: OpenAI } = await import('openai');
|
|
172
|
+
openaiClient = new OpenAI({ apiKey });
|
|
173
|
+
return openaiClient;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============== Main ==============
|
|
177
|
+
|
|
178
|
+
const conversations = new ConversationManager(CONFIG.maxHistory, CONFIG.conversationTimeoutMs);
|
|
179
|
+
const rateLimiter = new UserRateLimiter(CONFIG.rateLimitPerMinute);
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
console.log('Starting AI bot...');
|
|
183
|
+
|
|
184
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
185
|
+
console.warn('Warning: OPENAI_API_KEY not set. Bot will run but AI responses will fail.');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { state } = await useFileAuthState('./auth');
|
|
189
|
+
|
|
190
|
+
const sock = makeWASocket({
|
|
191
|
+
authState: state,
|
|
192
|
+
printQRInTerminal: true,
|
|
193
|
+
ramOptimizationLevel: 3, // Aggressive untuk hemat RAM karena ada OpenAI context
|
|
194
|
+
maxReconnectAttempts: 10
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
sock.ev.on('connection.update', (update) => {
|
|
198
|
+
const { connection, qr } = update;
|
|
199
|
+
if (qr) console.log('Scan QR dengan WhatsApp mobile...');
|
|
200
|
+
if (connection === 'open') {
|
|
201
|
+
console.log('✓ AI bot online sebagai', sock.user?.id);
|
|
202
|
+
console.log(`Using model: ${CONFIG.model}`);
|
|
203
|
+
}
|
|
204
|
+
if (connection === 'close') console.log('Connection closed, reconnecting...');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
sock.ev.on('message.upsert', async ({ messages, type }) => {
|
|
208
|
+
if (type !== 'notify') return;
|
|
209
|
+
for (const message of messages) {
|
|
210
|
+
try {
|
|
211
|
+
await handleMessage(sock, message);
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
console.error('Error:', err.message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Cleanup expired conversations tiap 5 menit (hemat RAM)
|
|
219
|
+
setInterval(() => {
|
|
220
|
+
conversations.cleanupAll();
|
|
221
|
+
rateLimiter.cleanup();
|
|
222
|
+
console.log(`Active conversations: ${conversations.size()}`);
|
|
223
|
+
}, 5 * 60 * 1000);
|
|
224
|
+
|
|
225
|
+
console.log('AI bot ready. Send a message to start chatting!');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============== Message Handler ==============
|
|
229
|
+
|
|
230
|
+
async function handleMessage(sock: ReturnType<typeof makeWASocket>, message: WAMessage) {
|
|
231
|
+
if (message.key.fromMe) return;
|
|
232
|
+
|
|
233
|
+
const from = message.key.remoteJid;
|
|
234
|
+
const userKey = jidToString(from).split(':')[0];
|
|
235
|
+
const text = extractMessageText(message.message) || '';
|
|
236
|
+
const pushName = message.pushName || 'User';
|
|
237
|
+
|
|
238
|
+
if (!text) {
|
|
239
|
+
// Check for media (image for vision)
|
|
240
|
+
if (hasMedia(message.message)) {
|
|
241
|
+
await handleMedia(sock, from, message);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(`[${from}] ${pushName}: ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`);
|
|
247
|
+
|
|
248
|
+
// Auto read receipt
|
|
249
|
+
await sock.sendReadReceipt(from, [message.key.id]).catch(() => {});
|
|
250
|
+
|
|
251
|
+
// Send typing indicator
|
|
252
|
+
await sock.sendPresenceUpdate(from, 'composing').catch(() => {});
|
|
253
|
+
|
|
254
|
+
// Rate limit check
|
|
255
|
+
if (!rateLimiter.canProceed(userKey)) {
|
|
256
|
+
await sock.sendMessage(from, {
|
|
257
|
+
text: '⚠️ Kamu terlalu banyak nanya. Tunggu sebentar ya (rate limit).'
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Commands
|
|
263
|
+
if (text.startsWith('!')) {
|
|
264
|
+
await handleCommand(sock, from, text, message);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Generate AI response
|
|
269
|
+
try {
|
|
270
|
+
const response = await generateAIResponse(from, text, pushName);
|
|
271
|
+
await sock.sendMessage(from, { text: response });
|
|
272
|
+
} catch (err: any) {
|
|
273
|
+
console.error('AI response error:', err.message);
|
|
274
|
+
await sock.sendMessage(from, {
|
|
275
|
+
text: 'Maaf, ada error. Coba lagi nanti ya. Ketik !reset untuk reset conversation.'
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============== AI Generation ==============
|
|
281
|
+
|
|
282
|
+
async function generateAIResponse(
|
|
283
|
+
from: JIDLike,
|
|
284
|
+
userText: string,
|
|
285
|
+
userName: string
|
|
286
|
+
): Promise<string> {
|
|
287
|
+
const openai = await getOpenAIClient();
|
|
288
|
+
|
|
289
|
+
// Add user message ke history
|
|
290
|
+
conversations.addMessage(from, {
|
|
291
|
+
role: 'user',
|
|
292
|
+
content: userText,
|
|
293
|
+
timestamp: Date.now()
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Build messages array dengan system prompt
|
|
297
|
+
const systemMessage: ConversationMessage = {
|
|
298
|
+
role: 'system',
|
|
299
|
+
content: `${CONFIG.systemPrompt}\n\nUser name: ${userName}`,
|
|
300
|
+
timestamp: Date.now()
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const history = conversations.getHistory(from);
|
|
304
|
+
const messages = [
|
|
305
|
+
systemMessage,
|
|
306
|
+
...history.map(m => ({ role: m.role, content: m.content }))
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
// Call OpenAI
|
|
310
|
+
const response = await openai.chat.completions.create({
|
|
311
|
+
model: CONFIG.model,
|
|
312
|
+
messages,
|
|
313
|
+
max_tokens: CONFIG.maxTokens,
|
|
314
|
+
temperature: CONFIG.temperature
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const reply = response.choices[0]?.message?.content ?? 'Maaf, saya tidak paham.';
|
|
318
|
+
|
|
319
|
+
// Add assistant reply ke history
|
|
320
|
+
conversations.addMessage(from, {
|
|
321
|
+
role: 'assistant',
|
|
322
|
+
content: reply,
|
|
323
|
+
timestamp: Date.now()
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return reply;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ============== Media Handler ==============
|
|
330
|
+
|
|
331
|
+
async function handleMedia(
|
|
332
|
+
sock: ReturnType<typeof makeWASocket>,
|
|
333
|
+
from: JIDLike,
|
|
334
|
+
message: WAMessage
|
|
335
|
+
) {
|
|
336
|
+
try {
|
|
337
|
+
// Check if image
|
|
338
|
+
if (message.message?.image) {
|
|
339
|
+
await sock.sendMessage(from, { text: '👀 Melihat gambar...' });
|
|
340
|
+
|
|
341
|
+
// Download image
|
|
342
|
+
const mediaInfo = message.message.image;
|
|
343
|
+
if (!mediaInfo.stream && !mediaInfo.url) {
|
|
344
|
+
await sock.sendMessage(from, { text: '❌ Tidak bisa download gambar' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Download dan convert ke base64
|
|
349
|
+
// Note: actual download requires mediaKey from message - simplified here
|
|
350
|
+
// const buffer = await sock.downloadMedia(url, mediaKey, 'image', 'image/jpeg');
|
|
351
|
+
// const base64 = buffer.toString('base64');
|
|
352
|
+
|
|
353
|
+
// For now, just acknowledge
|
|
354
|
+
await sock.sendMessage(from, {
|
|
355
|
+
text: '✓ Gambar diterima. Image analysis akan diimplementasikan.'
|
|
356
|
+
});
|
|
357
|
+
} else if (message.message?.audio) {
|
|
358
|
+
await sock.sendMessage(from, { text: '🎧 Voice message diterima. Ketik !transcribe untuk transcribe.' });
|
|
359
|
+
}
|
|
360
|
+
} catch (err: any) {
|
|
361
|
+
console.error('Media handler error:', err.message);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ============== Commands ==============
|
|
366
|
+
|
|
367
|
+
async function handleCommand(
|
|
368
|
+
sock: ReturnType<typeof makeWASocket>,
|
|
369
|
+
from: JIDLike,
|
|
370
|
+
text: string,
|
|
371
|
+
message: WAMessage
|
|
372
|
+
) {
|
|
373
|
+
const [cmd, ...args] = text.slice(1).split(' ');
|
|
374
|
+
const cmdLower = cmd.toLowerCase();
|
|
375
|
+
|
|
376
|
+
switch (cmdLower) {
|
|
377
|
+
case 'reset':
|
|
378
|
+
conversations.reset(from);
|
|
379
|
+
await sock.sendMessage(from, { text: '✓ Conversation context direset' });
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'help':
|
|
383
|
+
await sock.sendMessage(from, {
|
|
384
|
+
text: [
|
|
385
|
+
'*AI Bot Commands*',
|
|
386
|
+
'',
|
|
387
|
+
'!reset - Reset conversation context',
|
|
388
|
+
'!help - Tampilkan bantuan ini',
|
|
389
|
+
'!image <prompt> - Generate image dengan DALL-E',
|
|
390
|
+
'!transcribe - Transcribe voice message terakhir',
|
|
391
|
+
'!info - Info bot & statistik',
|
|
392
|
+
'',
|
|
393
|
+
'Atau kirim pesan apapun untuk chat dengan AI!'
|
|
394
|
+
].join('\n')
|
|
395
|
+
});
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'info':
|
|
399
|
+
await sock.sendMessage(from, {
|
|
400
|
+
text: [
|
|
401
|
+
'*Bot Info*',
|
|
402
|
+
`Model: ${CONFIG.model}`,
|
|
403
|
+
`Max tokens: ${CONFIG.maxTokens}`,
|
|
404
|
+
`Temperature: ${CONFIG.temperature}`,
|
|
405
|
+
`Active conversations: ${conversations.size()}`,
|
|
406
|
+
`Rate limit: ${CONFIG.rateLimitPerMinute}/min per user`
|
|
407
|
+
].join('\n')
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'image':
|
|
412
|
+
const prompt = args.join(' ');
|
|
413
|
+
if (!prompt) {
|
|
414
|
+
await sock.sendMessage(from, { text: 'Usage: !image <prompt>' });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
await generateAndSendImage(sock, from, prompt);
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case 'transcribe':
|
|
421
|
+
await sock.sendMessage(from, {
|
|
422
|
+
text: 'Voice transcription akan diimplementasikan. Untuk sekarang, ketik ulang pesannya ya.'
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
|
|
426
|
+
default:
|
|
427
|
+
// Unknown command - treat as regular message
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function generateAndSendImage(
|
|
433
|
+
sock: ReturnType<typeof makeWASocket>,
|
|
434
|
+
from: JIDLike,
|
|
435
|
+
prompt: string
|
|
436
|
+
) {
|
|
437
|
+
try {
|
|
438
|
+
const openai = await getOpenAIClient();
|
|
439
|
+
await sock.sendMessage(from, { text: `🎨 Generating image: "${prompt}"...` });
|
|
440
|
+
|
|
441
|
+
const response = await openai.images.generate({
|
|
442
|
+
model: 'dall-e-3',
|
|
443
|
+
prompt,
|
|
444
|
+
n: 1,
|
|
445
|
+
size: '1024x1024',
|
|
446
|
+
response_format: 'b64_json'
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const base64 = response.data[0]?.b64_json;
|
|
450
|
+
if (!base64) {
|
|
451
|
+
await sock.sendMessage(from, { text: '❌ Gagal generate image' });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
456
|
+
|
|
457
|
+
// Upload ke WhatsApp dan kirim
|
|
458
|
+
// Note: Actual upload via sock.uploadMedia - simplified here
|
|
459
|
+
await sock.sendMessage(from, {
|
|
460
|
+
image: { stream: buffer as any, mimetype: 'image/png' },
|
|
461
|
+
caption: `✨ Generated: "${prompt}"`
|
|
462
|
+
}).catch(async (err: any) => {
|
|
463
|
+
console.error('Image send failed:', err.message);
|
|
464
|
+
await sock.sendMessage(from, {
|
|
465
|
+
text: `✓ Image generated (but failed to send). URL: ${response.data[0]?.url ?? 'N/A'}`
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
} catch (err: any) {
|
|
469
|
+
console.error('Image gen error:', err.message);
|
|
470
|
+
await sock.sendMessage(from, { text: `❌ Error: ${err.message}` });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ============== Run ==============
|
|
475
|
+
|
|
476
|
+
main().catch(err => {
|
|
477
|
+
console.error('Fatal error:', err);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "waliwa-ai-bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WhatsApp bot dengan GPT integration untuk conversational AI",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "ts-node index.ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"waliwa": "file:../../"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^20.11.0",
|
|
14
|
+
"typescript": "^5.3.3",
|
|
15
|
+
"ts-node": "^10.9.2"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Echo Bot Example - Basic WhatsApp bot menggunakan Waliwa
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Auto scan QR (atau pakai existing session)
|
|
6
|
+
* - Reply dengan echo untuk setiap pesan masuk
|
|
7
|
+
* - Command: !ping, !echo <text>, !help
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npm install
|
|
11
|
+
* npm start
|
|
12
|
+
*
|
|
13
|
+
* Scan QR yang muncul di terminal dengan WhatsApp mobile app.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { makeWASocket, useFileAuthState, type WAMessage } from 'waliwa';
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
// Initialize auth state - simpan ke folder ./auth
|
|
20
|
+
const { state } = await useFileAuthState('./auth');
|
|
21
|
+
|
|
22
|
+
// Create socket dengan QR print ke terminal
|
|
23
|
+
const sock = makeWASocket({
|
|
24
|
+
authState: state,
|
|
25
|
+
printQRInTerminal: true,
|
|
26
|
+
// RAM optimization level: 2 = balanced, 3 = aggressive (untuk low-end devices)
|
|
27
|
+
ramOptimizationLevel: 2,
|
|
28
|
+
// Auto reconnect on disconnect
|
|
29
|
+
maxReconnectAttempts: 10,
|
|
30
|
+
reconnectInterval: 2000
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Connection status
|
|
34
|
+
sock.ev.on('connection.update', (update) => {
|
|
35
|
+
const { connection, lastDisconnect, qr } = update;
|
|
36
|
+
|
|
37
|
+
if (qr) {
|
|
38
|
+
console.log('\n=== QR Code tersedia - scan dengan WhatsApp mobile ===\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (connection === 'connecting') {
|
|
42
|
+
console.log('Connecting ke WhatsApp server...');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (connection === 'open') {
|
|
46
|
+
console.log('\n=== Bot terhubung! ===');
|
|
47
|
+
console.log(`Logged in sebagai: ${sock.user?.id ?? 'unknown'}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (connection === 'close') {
|
|
51
|
+
const reason = lastDisconnect?.output;
|
|
52
|
+
console.log('Connection closed:', reason);
|
|
53
|
+
// Reconnect jika bukan intentional
|
|
54
|
+
if (reason?.code !== 1000) {
|
|
55
|
+
console.log('Akan reconnect otomatis...');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Handle incoming messages
|
|
61
|
+
sock.ev.on('message.upsert', async ({ messages, type }) => {
|
|
62
|
+
// Hanya proses pesan baru (bukan history sync)
|
|
63
|
+
if (type !== 'notify') return;
|
|
64
|
+
|
|
65
|
+
for (const message of messages) {
|
|
66
|
+
await handleMessage(sock, message);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Handle message status updates (read receipts)
|
|
71
|
+
sock.ev.on('message.update', ({ messages }) => {
|
|
72
|
+
for (const msg of messages) {
|
|
73
|
+
if (msg.status === 'read') {
|
|
74
|
+
console.log(`Message ${msg.key.id} was read`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log('Echo bot started. Waiting for messages...');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleMessage(sock: ReturnType<typeof makeWASocket>, message: WAMessage) {
|
|
83
|
+
try {
|
|
84
|
+
// Skip jika pesan dari diri sendiri
|
|
85
|
+
if (message.key.fromMe) return;
|
|
86
|
+
|
|
87
|
+
const from = message.key.remoteJid;
|
|
88
|
+
const text = message.message?.text || '';
|
|
89
|
+
const pushName = message.pushName || 'Unknown';
|
|
90
|
+
|
|
91
|
+
// Skip jika tidak ada text
|
|
92
|
+
if (!text) return;
|
|
93
|
+
|
|
94
|
+
console.log(`[${from}] ${pushName}: ${text}`);
|
|
95
|
+
|
|
96
|
+
// Auto read receipt
|
|
97
|
+
await sock.sendReadReceipt(from, [message.key.id]).catch(() => {});
|
|
98
|
+
|
|
99
|
+
// Parse command
|
|
100
|
+
const [cmd, ...args] = text.split(' ');
|
|
101
|
+
const cmdLower = cmd.toLowerCase();
|
|
102
|
+
|
|
103
|
+
switch (cmdLower) {
|
|
104
|
+
case '!ping':
|
|
105
|
+
await sock.sendMessage(from, { text: 'pong! 🏓' });
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case '!echo':
|
|
109
|
+
if (args.length === 0) {
|
|
110
|
+
await sock.sendMessage(from, { text: 'Usage: !echo <text>' });
|
|
111
|
+
} else {
|
|
112
|
+
await sock.sendMessage(from, { text: args.join(' ') });
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case '!help':
|
|
117
|
+
await sock.sendMessage(from, {
|
|
118
|
+
text: [
|
|
119
|
+
'*Echo Bot Commands*',
|
|
120
|
+
'',
|
|
121
|
+
'!ping - Check if bot alive',
|
|
122
|
+
'!echo <text> - Reply dengan text yang sama',
|
|
123
|
+
'!help - Tampilkan bantuan ini',
|
|
124
|
+
'!info - Info bot',
|
|
125
|
+
'!time - Jam server',
|
|
126
|
+
''
|
|
127
|
+
].join('\n')
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case '!info':
|
|
132
|
+
await sock.sendMessage(from, {
|
|
133
|
+
text: [
|
|
134
|
+
'*Bot Info*',
|
|
135
|
+
`Library: Waliwa v1.0.0`,
|
|
136
|
+
`Protocol: WhatsApp Web MD`,
|
|
137
|
+
`Status: Online`,
|
|
138
|
+
`RAM Mode: Balanced`
|
|
139
|
+
].join('\n')
|
|
140
|
+
});
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case '!time':
|
|
144
|
+
const now = new Date();
|
|
145
|
+
await sock.sendMessage(from, {
|
|
146
|
+
text: `Server time: ${now.toLocaleString('id-ID')}`
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
// Echo semua pesan lain
|
|
152
|
+
await sock.sendMessage(from, {
|
|
153
|
+
text: `Echo: ${text}`,
|
|
154
|
+
quoted: message
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
console.error('Error handling message:', err.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Graceful shutdown
|
|
163
|
+
process.on('SIGINT', async () => {
|
|
164
|
+
console.log('\nShutting down...');
|
|
165
|
+
process.exit(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
main().catch(err => {
|
|
169
|
+
console.error('Fatal error:', err);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "waliwa-echo-bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Basic echo bot menggunakan Waliwa library",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "ts-node index.ts",
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"waliwa": "file:../../"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^20.11.0",
|
|
15
|
+
"typescript": "^5.3.3",
|
|
16
|
+
"ts-node": "^10.9.2"
|
|
17
|
+
}
|
|
18
|
+
}
|