@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,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API Gateway Example - HTTP endpoint untuk kirim pesan WhatsApp
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - POST /send-text - Send text message
|
|
6
|
+
* - POST /send-media - Send image/video/audio
|
|
7
|
+
* - GET /status - Get connection status
|
|
8
|
+
* - GET /health - Health check
|
|
9
|
+
* - POST /webhook - Set webhook untuk incoming messages
|
|
10
|
+
* - GET /groups - List groups
|
|
11
|
+
* - POST /groups/create - Create new group
|
|
12
|
+
* - GET /contacts - List known contacts
|
|
13
|
+
*
|
|
14
|
+
* Setup:
|
|
15
|
+
* 1. npm install
|
|
16
|
+
* 2. npm start
|
|
17
|
+
* 3. Scan QR yang muncul
|
|
18
|
+
* 4. Setelah connected, test dengan curl:
|
|
19
|
+
*
|
|
20
|
+
* curl -X POST http://localhost:3000/send-text \
|
|
21
|
+
* -H "Content-Type: application/json" \
|
|
22
|
+
* -d '{"to":"6281234567890","message":"Hello from API!"}'
|
|
23
|
+
*
|
|
24
|
+
* Security:
|
|
25
|
+
* - Set API_KEY environment variable untuk auth (Bearer token)
|
|
26
|
+
* - Set ALLOWED_IPS untuk IP whitelist
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
30
|
+
import {
|
|
31
|
+
makeWASocket,
|
|
32
|
+
useFileAuthState,
|
|
33
|
+
isGroupJid,
|
|
34
|
+
jidToString,
|
|
35
|
+
type WAMessage,
|
|
36
|
+
type JIDLike
|
|
37
|
+
} from 'waliwa';
|
|
38
|
+
|
|
39
|
+
// ============== Config ==============
|
|
40
|
+
|
|
41
|
+
const CONFIG = {
|
|
42
|
+
port: parseInt(process.env.PORT ?? '3000', 10),
|
|
43
|
+
apiKey: process.env.API_KEY,
|
|
44
|
+
allowedIps: process.env.ALLOWED_IPS?.split(',').map(ip => ip.trim()) ?? [],
|
|
45
|
+
webhookUrl: process.env.WEBHOOK_URL,
|
|
46
|
+
rateLimitPerMinute: parseInt(process.env.RATE_LIMIT ?? '60', 10),
|
|
47
|
+
bodyLimit: '10mb'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ============== App State ==============
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(express.json({ limit: CONFIG.bodyLimit }));
|
|
54
|
+
app.use(express.urlencoded({ extended: true, limit: CONFIG.bodyLimit }));
|
|
55
|
+
|
|
56
|
+
let sock: ReturnType<typeof makeWASocket> | null = null;
|
|
57
|
+
let isReady = false;
|
|
58
|
+
|
|
59
|
+
// Rate limiter per IP
|
|
60
|
+
const rateLimiter = new Map<string, { count: number; resetAt: number }>();
|
|
61
|
+
|
|
62
|
+
// Webhook subscribers
|
|
63
|
+
const webhooks = new Set<string>();
|
|
64
|
+
|
|
65
|
+
// ============== Auth Middleware ==============
|
|
66
|
+
|
|
67
|
+
function authMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
68
|
+
// Check IP whitelist
|
|
69
|
+
if (CONFIG.allowedIps.length > 0) {
|
|
70
|
+
const clientIp = req.ip ?? req.socket.remoteAddress ?? '';
|
|
71
|
+
const cleanIp = clientIp.replace('::ffff:', '');
|
|
72
|
+
if (!CONFIG.allowedIps.includes(cleanIp)) {
|
|
73
|
+
res.status(403).json({ error: 'IP not allowed' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check API key
|
|
79
|
+
if (CONFIG.apiKey) {
|
|
80
|
+
const auth = req.headers.authorization;
|
|
81
|
+
if (auth !== `Bearer ${CONFIG.apiKey}`) {
|
|
82
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Rate limit
|
|
88
|
+
const clientKey = req.ip ?? 'unknown';
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
let entry = rateLimiter.get(clientKey);
|
|
91
|
+
if (!entry || now > entry.resetAt) {
|
|
92
|
+
entry = { count: 0, resetAt: now + 60000 };
|
|
93
|
+
rateLimiter.set(clientKey, entry);
|
|
94
|
+
}
|
|
95
|
+
if (entry.count >= CONFIG.rateLimitPerMinute) {
|
|
96
|
+
res.status(429).json({ error: 'Rate limit exceeded' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
entry.count++;
|
|
100
|
+
|
|
101
|
+
next();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============== WhatsApp Connection ==============
|
|
105
|
+
|
|
106
|
+
async function startWhatsApp() {
|
|
107
|
+
console.log('Starting WhatsApp connection...');
|
|
108
|
+
const { state } = await useFileAuthState('./auth');
|
|
109
|
+
|
|
110
|
+
sock = makeWASocket({
|
|
111
|
+
authState: state,
|
|
112
|
+
printQRInTerminal: true,
|
|
113
|
+
ramOptimizationLevel: 2,
|
|
114
|
+
maxReconnectAttempts: 10
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
sock.ev.on('connection.update', (update) => {
|
|
118
|
+
const { connection, qr } = update;
|
|
119
|
+
if (qr) console.log('Scan QR dengan WhatsApp mobile...');
|
|
120
|
+
if (connection === 'open') {
|
|
121
|
+
console.log('✓ WhatsApp connected as', sock!.user?.id);
|
|
122
|
+
isReady = true;
|
|
123
|
+
}
|
|
124
|
+
if (connection === 'close') {
|
|
125
|
+
console.log('Connection closed, reconnecting...');
|
|
126
|
+
isReady = false;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Forward incoming messages ke webhook subscribers
|
|
131
|
+
sock.ev.on('message.upsert', async ({ messages }) => {
|
|
132
|
+
for (const message of messages) {
|
|
133
|
+
await forwardToWebhooks(message);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Forward call events
|
|
138
|
+
sock.ev.on('calls', async (callEvent) => {
|
|
139
|
+
await forwardToWebhooks(callEvent, 'call');
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============== Webhook Forwarding ==============
|
|
144
|
+
|
|
145
|
+
async function forwardToWebhooks(payload: any, type: string = 'message'): Promise<void> {
|
|
146
|
+
if (webhooks.size === 0) return;
|
|
147
|
+
|
|
148
|
+
const body = JSON.stringify({
|
|
149
|
+
type,
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
data: payload
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
for (const url of webhooks) {
|
|
155
|
+
try {
|
|
156
|
+
await fetch(url, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: { 'Content-Type': 'application/json' },
|
|
159
|
+
body
|
|
160
|
+
});
|
|
161
|
+
} catch (err: any) {
|
|
162
|
+
console.error(`Webhook ${url} failed:`, err.message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============== API Routes ==============
|
|
168
|
+
|
|
169
|
+
// Health check
|
|
170
|
+
app.get('/health', (req: Request, res: Response) => {
|
|
171
|
+
res.json({ status: 'ok', timestamp: Date.now() });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Connection status
|
|
175
|
+
app.get('/status', authMiddleware, (req: Request, res: Response) => {
|
|
176
|
+
res.json({
|
|
177
|
+
connected: isReady,
|
|
178
|
+
user: sock?.user,
|
|
179
|
+
timestamp: Date.now()
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Send text message
|
|
184
|
+
app.post('/send-text', authMiddleware, async (req: Request, res: Response) => {
|
|
185
|
+
if (!isReady || !sock) {
|
|
186
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { to, message, mentions, quotedId } = req.body;
|
|
191
|
+
|
|
192
|
+
if (!to || !message) {
|
|
193
|
+
res.status(400).json({ error: 'Missing required fields: to, message' });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const jid = normalizeJid(to);
|
|
199
|
+
const content: any = { text: message };
|
|
200
|
+
|
|
201
|
+
if (mentions && Array.isArray(mentions)) {
|
|
202
|
+
content.mentions = mentions.map((m: string) => normalizeJid(m));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await sock.sendMessage(jid, content);
|
|
206
|
+
res.json({
|
|
207
|
+
success: true,
|
|
208
|
+
messageId: result.key.id,
|
|
209
|
+
status: result.status,
|
|
210
|
+
timestamp: result.messageTimestamp
|
|
211
|
+
});
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
res.status(500).json({ error: err.message });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Send media (image, video, audio, document)
|
|
218
|
+
app.post('/send-media', authMiddleware, async (req: Request, res: Response) => {
|
|
219
|
+
if (!isReady || !sock) {
|
|
220
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { to, type, base64, mimetype, caption, fileName } = req.body;
|
|
225
|
+
|
|
226
|
+
if (!to || !type || !base64 || !mimetype) {
|
|
227
|
+
res.status(400).json({ error: 'Missing required fields: to, type, base64, mimetype' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const jid = normalizeJid(to);
|
|
233
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
234
|
+
|
|
235
|
+
// First upload ke WhatsApp CDN
|
|
236
|
+
const uploadResult = await sock.uploadMedia(buffer, type, mimetype, fileName);
|
|
237
|
+
|
|
238
|
+
// Then send message dengan media reference
|
|
239
|
+
let content: any;
|
|
240
|
+
switch (type) {
|
|
241
|
+
case 'image':
|
|
242
|
+
content = {
|
|
243
|
+
image: { stream: buffer as any, mimetype },
|
|
244
|
+
caption
|
|
245
|
+
};
|
|
246
|
+
break;
|
|
247
|
+
case 'video':
|
|
248
|
+
content = {
|
|
249
|
+
video: { stream: buffer as any, mimetype },
|
|
250
|
+
caption
|
|
251
|
+
};
|
|
252
|
+
break;
|
|
253
|
+
case 'audio':
|
|
254
|
+
content = {
|
|
255
|
+
audio: { stream: buffer as any, mimetype }
|
|
256
|
+
};
|
|
257
|
+
break;
|
|
258
|
+
case 'document':
|
|
259
|
+
content = {
|
|
260
|
+
document: { stream: buffer as any, mimetype, fileName }
|
|
261
|
+
};
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
res.status(400).json({ error: `Unknown media type: ${type}` });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await sock.sendMessage(jid, content);
|
|
269
|
+
res.json({
|
|
270
|
+
success: true,
|
|
271
|
+
messageId: result.key.id,
|
|
272
|
+
status: result.status,
|
|
273
|
+
mediaUrl: uploadResult.url,
|
|
274
|
+
timestamp: result.messageTimestamp
|
|
275
|
+
});
|
|
276
|
+
} catch (err: any) {
|
|
277
|
+
res.status(500).json({ error: err.message });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Send reaction
|
|
282
|
+
app.post('/send-reaction', authMiddleware, async (req: Request, res: Response) => {
|
|
283
|
+
if (!isReady || !sock) {
|
|
284
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { to, messageId, emoji } = req.body;
|
|
289
|
+
if (!to || !messageId || !emoji) {
|
|
290
|
+
res.status(400).json({ error: 'Missing required fields: to, messageId, emoji' });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const result = await sock.reactToMessage(
|
|
296
|
+
normalizeJid(to),
|
|
297
|
+
{ remoteJid: normalizeJid(to), fromMe: false, id: messageId },
|
|
298
|
+
emoji
|
|
299
|
+
);
|
|
300
|
+
res.json({ success: true, status: result.status });
|
|
301
|
+
} catch (err: any) {
|
|
302
|
+
res.status(500).json({ error: err.message });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Delete message
|
|
307
|
+
app.delete('/message', authMiddleware, async (req: Request, res: Response) => {
|
|
308
|
+
if (!isReady || !sock) {
|
|
309
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { to, messageId, revokeEveryone } = req.body;
|
|
314
|
+
if (!to || !messageId) {
|
|
315
|
+
res.status(400).json({ error: 'Missing required fields: to, messageId' });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const result = await sock.deleteMessage(
|
|
321
|
+
normalizeJid(to),
|
|
322
|
+
{ remoteJid: normalizeJid(to), fromMe: true, id: messageId },
|
|
323
|
+
{ revokeEveryone: revokeEveryone ?? false }
|
|
324
|
+
);
|
|
325
|
+
res.json({ success: true, status: result.status });
|
|
326
|
+
} catch (err: any) {
|
|
327
|
+
res.status(500).json({ error: err.message });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// List groups
|
|
332
|
+
app.get('/groups', authMiddleware, async (req: Request, res: Response) => {
|
|
333
|
+
if (!isReady || !sock) {
|
|
334
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Note: Real implementation perlu query groups dari sync
|
|
339
|
+
// Simplified untuk example
|
|
340
|
+
res.json({
|
|
341
|
+
success: true,
|
|
342
|
+
groups: [],
|
|
343
|
+
note: 'Use specific group ID via /groups/:id for metadata'
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Get group metadata
|
|
348
|
+
app.get('/groups/:jid', authMiddleware, async (req: Request, res: Response) => {
|
|
349
|
+
if (!isReady || !sock) {
|
|
350
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const metadata = await sock.groupMetadata(req.params.jid);
|
|
356
|
+
res.json({ success: true, metadata });
|
|
357
|
+
} catch (err: any) {
|
|
358
|
+
res.status(500).json({ error: err.message });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Create group
|
|
363
|
+
app.post('/groups/create', authMiddleware, async (req: Request, res: Response) => {
|
|
364
|
+
if (!isReady || !sock) {
|
|
365
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { subject, participants, description } = req.body;
|
|
370
|
+
if (!subject || !participants || !Array.isArray(participants)) {
|
|
371
|
+
res.status(400).json({ error: 'Missing required: subject, participants[]' });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const metadata = await sock.groupCreate({
|
|
377
|
+
subject,
|
|
378
|
+
participants: participants.map((p: string) => normalizeJid(p)),
|
|
379
|
+
desc: description
|
|
380
|
+
});
|
|
381
|
+
res.json({ success: true, groupId: metadata.id, subject: metadata.subject });
|
|
382
|
+
} catch (err: any) {
|
|
383
|
+
res.status(500).json({ error: err.message });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Group participants management
|
|
388
|
+
app.post('/groups/:jid/participants', authMiddleware, async (req: Request, res: Response) => {
|
|
389
|
+
if (!isReady || !sock) {
|
|
390
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const { participants, action } = req.body;
|
|
395
|
+
if (!participants || !action) {
|
|
396
|
+
res.status(400).json({ error: 'Missing required: participants[], action' });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!['add', 'remove', 'promote', 'demote'].includes(action)) {
|
|
401
|
+
res.status(400).json({ error: 'Invalid action' });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
await sock.groupParticipantsUpdate(
|
|
407
|
+
req.params.jid,
|
|
408
|
+
participants.map((p: string) => normalizeJid(p)),
|
|
409
|
+
action
|
|
410
|
+
);
|
|
411
|
+
res.json({ success: true });
|
|
412
|
+
} catch (err: any) {
|
|
413
|
+
res.status(500).json({ error: err.message });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Get invite code
|
|
418
|
+
app.get('/groups/:jid/invite', authMiddleware, async (req: Request, res: Response) => {
|
|
419
|
+
if (!isReady || !sock) {
|
|
420
|
+
res.status(503).json({ error: 'WhatsApp not connected' });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const code = await sock.groupInviteCode(req.params.jid);
|
|
425
|
+
res.json({ success: true, code, url: `https://chat.whatsapp.com/${code}` });
|
|
426
|
+
} catch (err: any) {
|
|
427
|
+
res.status(500).json({ error: err.message });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Webhook management
|
|
432
|
+
app.post('/webhook', authMiddleware, (req: Request, res: Response) => {
|
|
433
|
+
const { url, action } = req.body;
|
|
434
|
+
if (!url) {
|
|
435
|
+
res.status(400).json({ error: 'Missing required: url' });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (action === 'remove') {
|
|
439
|
+
webhooks.delete(url);
|
|
440
|
+
} else {
|
|
441
|
+
webhooks.add(url);
|
|
442
|
+
}
|
|
443
|
+
res.json({ success: true, webhooks: Array.from(webhooks) });
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
app.get('/webhooks', authMiddleware, (req: Request, res: Response) => {
|
|
447
|
+
res.json({ webhooks: Array.from(webhooks) });
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ============== Helpers ==============
|
|
451
|
+
|
|
452
|
+
function normalizeJid(input: string): string {
|
|
453
|
+
// Remove non-digit chars dari phone number
|
|
454
|
+
const clean = input.replace(/[^0-9]/g, '');
|
|
455
|
+
// Jika sudah ada @, pakai apa adanya
|
|
456
|
+
if (input.includes('@')) return input;
|
|
457
|
+
// Jika 628... format, default ke s.whatsapp.net
|
|
458
|
+
return `${clean}@s.whatsapp.net`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============== Error Handler ==============
|
|
462
|
+
|
|
463
|
+
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
464
|
+
console.error('Unhandled error:', err.message);
|
|
465
|
+
res.status(500).json({ error: 'Internal server error', message: err.message });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// ============== Start Server ==============
|
|
469
|
+
|
|
470
|
+
async function main() {
|
|
471
|
+
await startWhatsApp();
|
|
472
|
+
|
|
473
|
+
app.listen(CONFIG.port, () => {
|
|
474
|
+
console.log(`\n=== Waliwa REST API Gateway ===`);
|
|
475
|
+
console.log(`Listening on http://localhost:${CONFIG.port}`);
|
|
476
|
+
console.log(`Auth: ${CONFIG.apiKey ? 'API Key required' : 'No auth (set API_KEY env var)'}`);
|
|
477
|
+
console.log(`IP whitelist: ${CONFIG.allowedIps.length > 0 ? CONFIG.allowedIps.join(', ') : 'disabled'}`);
|
|
478
|
+
console.log(`Rate limit: ${CONFIG.rateLimitPerMinute}/min per IP`);
|
|
479
|
+
console.log(`\nEndpoints:`);
|
|
480
|
+
console.log(` GET /health`);
|
|
481
|
+
console.log(` GET /status`);
|
|
482
|
+
console.log(` POST /send-text`);
|
|
483
|
+
console.log(` POST /send-media`);
|
|
484
|
+
console.log(` POST /send-reaction`);
|
|
485
|
+
console.log(` DELETE /message`);
|
|
486
|
+
console.log(` GET /groups`);
|
|
487
|
+
console.log(` GET /groups/:jid`);
|
|
488
|
+
console.log(` POST /groups/create`);
|
|
489
|
+
console.log(` POST /groups/:jid/participants`);
|
|
490
|
+
console.log(` GET /groups/:jid/invite`);
|
|
491
|
+
console.log(` POST /webhook`);
|
|
492
|
+
console.log(` GET /webhooks\n`);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
main().catch(err => {
|
|
497
|
+
console.error('Fatal error:', err);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "waliwa-rest-gateway",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HTTP REST API gateway untuk kirim pesan WhatsApp via REST endpoints",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "ts-node index.ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"waliwa": "file:../../",
|
|
11
|
+
"express": "^4.18.2"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^20.11.0",
|
|
15
|
+
"@types/express": "^4.17.21",
|
|
16
|
+
"typescript": "^5.3.3",
|
|
17
|
+
"ts-node": "^10.9.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kangwifi-pro/waliwa",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight, RAM-efficient WhatsApp library with hybrid protocol (WhatsApp Web MD + Mobile API), Multi-Branch SKDM Recovery system, and 9+ Baileys issue fixes. More complete than Baileys, lighter than ZapoJS.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.cjs"
|
|
11
|
+
},
|
|
12
|
+
"./types": {
|
|
13
|
+
"import": "./dist/types/index.js",
|
|
14
|
+
"require": "./dist/types/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"dev": "ts-node examples/echo-bot/index.ts",
|
|
23
|
+
"lint": "eslint src --ext .ts",
|
|
24
|
+
"test": "jest",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"whatsapp",
|
|
29
|
+
"bot",
|
|
30
|
+
"wa-web",
|
|
31
|
+
"multi-device",
|
|
32
|
+
"reverse-engineering",
|
|
33
|
+
"baileys",
|
|
34
|
+
"zapojs",
|
|
35
|
+
"lightweight",
|
|
36
|
+
"ram-efficient",
|
|
37
|
+
"noise-protocol",
|
|
38
|
+
"protobuf",
|
|
39
|
+
"pairing-code",
|
|
40
|
+
"qr-code"
|
|
41
|
+
],
|
|
42
|
+
"author": "waliwa contributors",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=16.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@noble/curves": "^1.4.0",
|
|
49
|
+
"@noble/hashes": "^1.4.0",
|
|
50
|
+
"@noble/ciphers": "^0.5.1",
|
|
51
|
+
"protobufjs": "^7.3.0",
|
|
52
|
+
"ws": "^8.16.0",
|
|
53
|
+
"qrcode-terminal": "^0.12.0",
|
|
54
|
+
"axios": "^1.6.7"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^20.11.20",
|
|
58
|
+
"@types/ws": "^8.5.10",
|
|
59
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
60
|
+
"typescript": "^5.3.3",
|
|
61
|
+
"ts-node": "^10.9.2",
|
|
62
|
+
"eslint": "^8.56.0",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
|
64
|
+
"jest": "^29.7.0",
|
|
65
|
+
"ts-jest": "^29.1.2"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"openai": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"express": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|