andiwagateway 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/PUBLISH_GUIDE.md +151 -0
- package/README.md +173 -0
- package/package.json +38 -0
- package/src/core/client.js +65 -0
- package/src/core/http-client.js +71 -0
- package/src/index.js +78 -0
- package/src/plugins/otp.js +105 -0
- package/src/webhook/handler.js +625 -0
package/PUBLISH_GUIDE.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Panduan Publish SDK ke NPM
|
|
2
|
+
|
|
3
|
+
## 🚀 Langkah-langkah Publish ke NPM
|
|
4
|
+
|
|
5
|
+
### 1. Daftar Akun NPM (Gratis)
|
|
6
|
+
|
|
7
|
+
1. Buka https://www.npmjs.com/signup
|
|
8
|
+
2. Buat akun dengan username (contoh: `andiwa`)
|
|
9
|
+
3. Verifikasi email
|
|
10
|
+
|
|
11
|
+
### 2. Login di Terminal
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd /Users/andiyanto/Desktop/qr-wa/andi-wa-gateway-sdk
|
|
15
|
+
npm login
|
|
16
|
+
```
|
|
17
|
+
Masukkan:
|
|
18
|
+
- Username: (username NPM Anda)
|
|
19
|
+
- Password: (password NPM Anda)
|
|
20
|
+
- Email: (email verifikasi)
|
|
21
|
+
- OTP: (kode dari email/authenticator)
|
|
22
|
+
|
|
23
|
+
### 3. Test Package (Dry Run)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm publish --dry-run
|
|
27
|
+
```
|
|
28
|
+
Ini akan simulasi publish tanpa benar-benar upload. Cek apakah ada error.
|
|
29
|
+
|
|
30
|
+
### 4. Publish (Public - Gratis)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm publish --access public
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Sukses!** SDK sekarang ada di: `https://www.npmjs.com/package/andi-wa-gateway-sdk`
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🔄 Update SDK (Versi Baru)
|
|
41
|
+
|
|
42
|
+
Kalau ada perubahan di SDK, update versi dulu:
|
|
43
|
+
|
|
44
|
+
### Otomatis (Recommended)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Update patch version (1.0.0 -> 1.0.1)
|
|
48
|
+
npm version patch
|
|
49
|
+
|
|
50
|
+
# Update minor version (1.0.0 -> 1.1.0)
|
|
51
|
+
npm version minor
|
|
52
|
+
|
|
53
|
+
# Update major version (1.0.0 -> 2.0.0)
|
|
54
|
+
npm version major
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Lalu publish:
|
|
58
|
+
```bash
|
|
59
|
+
npm publish
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Manual
|
|
63
|
+
|
|
64
|
+
Edit `package.json`, ubah version:
|
|
65
|
+
```json
|
|
66
|
+
"version": "1.0.1"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Lalu:
|
|
70
|
+
```bash
|
|
71
|
+
npm publish
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 📦 Developer Cara Install
|
|
77
|
+
|
|
78
|
+
Setelah publish, developer bisa:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install andi-wa-gateway-sdk
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Lalu pakai:
|
|
85
|
+
```javascript
|
|
86
|
+
const sdk = require('andi-wa-gateway-sdk');
|
|
87
|
+
|
|
88
|
+
const wa = sdk.createClient({
|
|
89
|
+
apiKey: 'ak_live_xxxx'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await wa.sendMessage('628123456789', 'Halo!');
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## ❌ Unpublish (Kalau ada masalah)
|
|
98
|
+
|
|
99
|
+
**Hati-hati!** Hanya bisa unpublish dalam 24 jam pertama:
|
|
100
|
+
```bash
|
|
101
|
+
npm unpublish andi-wa-gateway-sdk --force
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Setelah 24 jam, harus kontak NPM support.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## ✅ Checklist Sebelum Publish
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
□ Nama package unik (andi-wa-gateway-sdk)
|
|
112
|
+
□ Version benar (1.0.0)
|
|
113
|
+
□ Description jelas
|
|
114
|
+
□ Keywords lengkap
|
|
115
|
+
□ Repository URL benar (opsional)
|
|
116
|
+
□ License MIT
|
|
117
|
+
□ .npmignore ada (exclude examples/)
|
|
118
|
+
□ axios di dependencies
|
|
119
|
+
□ Test dry-run berhasil
|
|
120
|
+
□ npm login berhasil
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🆘 Troubleshooting
|
|
126
|
+
|
|
127
|
+
### Error: "You do not have permission"
|
|
128
|
+
```bash
|
|
129
|
+
# Pastikan login npm whoami
|
|
130
|
+
# Kalau tidak keluar username, login ulang
|
|
131
|
+
npm login
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Error: "Package name too similar"
|
|
135
|
+
```bash
|
|
136
|
+
# Ganti nama di package.json
|
|
137
|
+
"name": "andi-wa-gateway-sdk-v2"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Error: "Failed to replace blob"
|
|
141
|
+
```bash
|
|
142
|
+
# Bersihkan cache
|
|
143
|
+
npm cache clean --force
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 📞 Butuh Bantuan?
|
|
149
|
+
|
|
150
|
+
- Dokumentasi NPM: https://docs.npmjs.com/
|
|
151
|
+
- Support: https://www.npmjs.com/support
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Andi WA Gateway SDK
|
|
2
|
+
|
|
3
|
+
> **SDK Official untuk Andi WA Gateway**
|
|
4
|
+
>
|
|
5
|
+
> Tinggal pasang API Key, langsung kirim pesan WhatsApp!
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🎯 Fitur
|
|
10
|
+
|
|
11
|
+
- ✅ **API Key Only** - Tidak perlu setup server, tinggal API Key
|
|
12
|
+
- ✅ **Base URL Built-in** - Sudah terkonfigurasi ke `andi-wa-gateway.up.railway.app`
|
|
13
|
+
- ✅ **OTP Plugin** - Kirim kode verifikasi dengan 1 baris kode
|
|
14
|
+
- ✅ **Webhook Handler** - Terima pesan masuk dengan mudah
|
|
15
|
+
- ✅ **Express Ready** - Middleware siap pakai
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install andi-wa-gateway-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🚀 Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Kirim Pesan (Basic)
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
const sdk = require('andi-wa-gateway-sdk');
|
|
33
|
+
|
|
34
|
+
// Inisialisasi - Cukup API Key!
|
|
35
|
+
const wa = sdk.createClient({
|
|
36
|
+
apiKey: 'ak_live_abc123xyz789' // Dari Panel Andi WA Gateway
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Kirim pesan
|
|
40
|
+
await wa.sendMessage('628123456789', 'Halo dari SDK!');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Kirim OTP (Plugin)
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const sdk = require('andi-wa-gateway-sdk');
|
|
47
|
+
|
|
48
|
+
const otp = sdk.createOtp({
|
|
49
|
+
apiKey: 'ak_live_abc123xyz789'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Kirim OTP
|
|
53
|
+
const result = await otp.send('628123456789', {
|
|
54
|
+
appName: 'MyApp'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log('Kode OTP:', result.code); // Simpan ke database!
|
|
58
|
+
|
|
59
|
+
// Verifikasi
|
|
60
|
+
const isValid = otp.verify(inputUser, result.code);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Webhook Handler (Terima Pesan)
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
const sdk = require('andi-wa-gateway-sdk');
|
|
67
|
+
const webhook = sdk.createWebhook();
|
|
68
|
+
|
|
69
|
+
// Handler pesan masuk
|
|
70
|
+
webhook.onMessage(async (msg) => {
|
|
71
|
+
console.log('Dari:', msg.from);
|
|
72
|
+
console.log('Isi:', msg.body);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Express middleware
|
|
76
|
+
app.post('/webhook/whatsapp', webhook.middleware());
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 📚 API Reference
|
|
82
|
+
|
|
83
|
+
### `createClient(config)`
|
|
84
|
+
|
|
85
|
+
Membuat instance client utama.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
const wa = sdk.createClient({
|
|
89
|
+
apiKey: 'your-api-key', // Required
|
|
90
|
+
sessionId: 'default-session', // Optional
|
|
91
|
+
baseUrl: 'https://custom-url.com' // Optional (override default)
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Methods:**
|
|
96
|
+
|
|
97
|
+
| Method | Description |
|
|
98
|
+
|--------|-------------|
|
|
99
|
+
| `wa.sendMessage(number, message, sessionId?)` | Kirim pesan teks |
|
|
100
|
+
| `wa.sendMedia(number, mediaUrl, caption?, sessionId?)` | Kirim file/media |
|
|
101
|
+
| `wa.getStatus(sessionId?)` | Cek status session |
|
|
102
|
+
| `wa.listSessions()` | Daftar semua session |
|
|
103
|
+
| `wa.registerWebhook(url, events?)` | Register webhook URL |
|
|
104
|
+
|
|
105
|
+
### `createOtp(config)`
|
|
106
|
+
|
|
107
|
+
Plugin untuk kirim OTP.
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
const otp = sdk.createOtp({
|
|
111
|
+
apiKey: 'your-api-key',
|
|
112
|
+
sessionId: 'otp-session'
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Methods:**
|
|
117
|
+
|
|
118
|
+
| Method | Description |
|
|
119
|
+
|--------|-------------|
|
|
120
|
+
| `otp.generateCode(length?)` | Generate kode random |
|
|
121
|
+
| `otp.send(number, options?)` | Kirim OTP |
|
|
122
|
+
| `otp.sendWithTemplate(number, template, options?)` | Kirim dengan template custom |
|
|
123
|
+
| `otp.verify(input, stored)` | Verifikasi kode |
|
|
124
|
+
|
|
125
|
+
### `createWebhook(config)`
|
|
126
|
+
|
|
127
|
+
Handler untuk webhook.
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const webhook = sdk.createWebhook({
|
|
131
|
+
secret: 'optional-secret' // Untuk verify signature
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Methods:**
|
|
136
|
+
|
|
137
|
+
| Method | Description |
|
|
138
|
+
|--------|-------------|
|
|
139
|
+
| `webhook.on(event, handler)` | Register handler untuk event |
|
|
140
|
+
| `webhook.onMessage(handler)` | Handler khusus pesan masuk |
|
|
141
|
+
| `webhook.middleware()` | Express middleware |
|
|
142
|
+
| `webhook.createRouter()` | Express router instance |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 💡 Contoh Lengkap
|
|
147
|
+
|
|
148
|
+
Lihat folder `examples/`:
|
|
149
|
+
|
|
150
|
+
- `basic-usage.js` - Kirim pesan & media
|
|
151
|
+
- `otp-plugin.js` - Sistem verifikasi OTP
|
|
152
|
+
- `express-webhook.js` - Express server dengan webhook
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 🔧 Environment Variables
|
|
157
|
+
|
|
158
|
+
```env
|
|
159
|
+
WA_GATEWAY_API_KEY=ak_live_abc123xyz789
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 📞 Support
|
|
165
|
+
|
|
166
|
+
- Panel Admin: https://andi-wa-gateway.up.railway.app
|
|
167
|
+
- API Docs: Lihat dokumentasi di panel
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📄 License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "andiwagateway",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official SDK for Andi WA Gateway - WhatsApp API integration made simple. Just add API Key and start sending messages.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"whatsapp",
|
|
8
|
+
"gateway",
|
|
9
|
+
"sdk",
|
|
10
|
+
"andi",
|
|
11
|
+
"otp",
|
|
12
|
+
"notification",
|
|
13
|
+
"webhook",
|
|
14
|
+
"baileys",
|
|
15
|
+
"messaging",
|
|
16
|
+
"api"
|
|
17
|
+
],
|
|
18
|
+
"author": "Andi <andi@andi-wa-gateway.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "commonjs",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=16.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"axios": "^1.6.0"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/andi/andi-wa-gateway-sdk.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/andi/andi-wa-gateway-sdk/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/andi/andi-wa-gateway-sdk#readme",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "echo 'Error: no test specified' && exit 1"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const createHttpClient = require('./http-client');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main SDK Client
|
|
5
|
+
* File ini adalah entry point untuk semua operasi SDK
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function createClient(config) {
|
|
9
|
+
const http = createHttpClient(config);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
config,
|
|
13
|
+
http,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Kirim pesan teks WhatsApp
|
|
17
|
+
* @param {string} number - Nomor tujuan (format: 628123456789 atau 08123456789)
|
|
18
|
+
* @param {string} message - Isi pesan
|
|
19
|
+
* @param {string} [sessionId] - Override session ID (opsional)
|
|
20
|
+
*/
|
|
21
|
+
async sendMessage(number, message, sessionId = null) {
|
|
22
|
+
const targetSession = sessionId || config.sessionId;
|
|
23
|
+
return http.sendMessage(targetSession, number, message);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Kirim media/file WhatsApp
|
|
28
|
+
* @param {string} number - Nomor tujuan
|
|
29
|
+
* @param {string} mediaUrl - URL file/media
|
|
30
|
+
* @param {string} [caption] - Keterangan media
|
|
31
|
+
* @param {string} [sessionId] - Override session ID
|
|
32
|
+
*/
|
|
33
|
+
async sendMedia(number, mediaUrl, caption = '', sessionId = null) {
|
|
34
|
+
const targetSession = sessionId || config.sessionId;
|
|
35
|
+
return http.sendMedia(targetSession, number, mediaUrl, caption);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cek status session WhatsApp
|
|
40
|
+
* @param {string} [sessionId] - Session ID (default: session dari config)
|
|
41
|
+
*/
|
|
42
|
+
async getStatus(sessionId = null) {
|
|
43
|
+
const targetSession = sessionId || config.sessionId;
|
|
44
|
+
return http.getSessionStatus(targetSession);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Daftar semua session
|
|
49
|
+
*/
|
|
50
|
+
async listSessions() {
|
|
51
|
+
return http.getSessions();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register webhook untuk terima pesan masuk
|
|
56
|
+
* @param {string} webhookUrl - URL endpoint webhook Anda
|
|
57
|
+
* @param {Array} [events] - Event yang didengar (default: ['messages.upsert'])
|
|
58
|
+
*/
|
|
59
|
+
async registerWebhook(webhookUrl, events = ['messages.upsert']) {
|
|
60
|
+
return http.registerWebhook(webhookUrl, events);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = createClient;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP Client untuk Andi WA Gateway
|
|
5
|
+
* File ini handle semua HTTP request ke API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function createHttpClient(config) {
|
|
9
|
+
const instance = axios.create({
|
|
10
|
+
baseURL: `${config.baseUrl}/api`,
|
|
11
|
+
headers: {
|
|
12
|
+
'Authorization': `x-api-key ${config.apiKey}`,
|
|
13
|
+
'Content-Type': 'application/json'
|
|
14
|
+
},
|
|
15
|
+
timeout: 30000
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
async sendMessage(sessionId, number, message) {
|
|
20
|
+
const response = await instance.post('/messages/send', {
|
|
21
|
+
sessionId,
|
|
22
|
+
number: formatNumber(number),
|
|
23
|
+
message
|
|
24
|
+
});
|
|
25
|
+
return response.data;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async sendMedia(sessionId, number, mediaUrl, caption = '') {
|
|
29
|
+
const response = await instance.post('/messages/send-media', {
|
|
30
|
+
sessionId,
|
|
31
|
+
number: formatNumber(number),
|
|
32
|
+
mediaUrl,
|
|
33
|
+
caption
|
|
34
|
+
});
|
|
35
|
+
return response.data;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async getSessionStatus(sessionId) {
|
|
39
|
+
const response = await instance.get(`/session/status/${sessionId}`);
|
|
40
|
+
return response.data;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async getSessions() {
|
|
44
|
+
const response = await instance.get('/sessions');
|
|
45
|
+
return response.data;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async registerWebhook(url, events = ['messages.upsert']) {
|
|
49
|
+
const response = await instance.post('/webhook/register', {
|
|
50
|
+
url,
|
|
51
|
+
events
|
|
52
|
+
});
|
|
53
|
+
return response.data;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async getApiKeys() {
|
|
57
|
+
const response = await instance.get('/keys');
|
|
58
|
+
return response.data;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatNumber(number) {
|
|
64
|
+
let clean = String(number).replace(/[\s\+-]/g, '');
|
|
65
|
+
if (clean.startsWith('0')) {
|
|
66
|
+
clean = '62' + clean.substring(1);
|
|
67
|
+
}
|
|
68
|
+
return clean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = createHttpClient;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Andi WA Gateway SDK
|
|
3
|
+
* Official SDK untuk integrasi dengan Andi WA Gateway
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const sdk = require('andi-wa-gateway-sdk');
|
|
7
|
+
* const wa = sdk.createClient({ apiKey: 'your-api-key' });
|
|
8
|
+
* await wa.sendMessage('628123456789', 'Hello!');
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const createClient = require('./core/client');
|
|
12
|
+
const createOtpPlugin = require('./plugins/otp');
|
|
13
|
+
const createWebhookHandler = require('./webhook/handler');
|
|
14
|
+
|
|
15
|
+
const DEFAULT_BASE_URL = 'https://andi-wa-gateway.up.railway.app';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create SDK Client
|
|
19
|
+
* @param {Object} config
|
|
20
|
+
* @param {string} config.apiKey - API Key dari Andi WA Gateway Panel
|
|
21
|
+
* @param {string} [config.baseUrl] - Optional: Override base URL (default: https://andi-wa-gateway.up.railway.app)
|
|
22
|
+
* @param {string} [config.sessionId] - Default session ID untuk kirim pesan
|
|
23
|
+
* @returns {Object} SDK Client instance
|
|
24
|
+
*/
|
|
25
|
+
function createClient(config = {}) {
|
|
26
|
+
if (!config.apiKey) {
|
|
27
|
+
throw new Error('API Key wajib diisi. Dapatkan dari Panel Andi WA Gateway.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const finalConfig = {
|
|
31
|
+
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
32
|
+
apiKey: config.apiKey,
|
|
33
|
+
sessionId: config.sessionId || 'default-session'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return createClient(finalConfig);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create OTP Plugin
|
|
41
|
+
* @param {Object} config
|
|
42
|
+
* @param {string} config.apiKey - API Key
|
|
43
|
+
* @param {string} [config.sessionId] - Session ID
|
|
44
|
+
* @returns {Object} OTP Plugin instance
|
|
45
|
+
*/
|
|
46
|
+
function createOtp(config = {}) {
|
|
47
|
+
if (!config.apiKey) {
|
|
48
|
+
throw new Error('API Key wajib diisi.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const finalConfig = {
|
|
52
|
+
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
53
|
+
apiKey: config.apiKey,
|
|
54
|
+
sessionId: config.sessionId || 'default-session'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return createOtpPlugin(finalConfig);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create Webhook Handler
|
|
62
|
+
* @param {Object} config
|
|
63
|
+
* @param {string} [config.secret] - Webhook secret untuk verifikasi
|
|
64
|
+
* @returns {Object} Webhook Handler instance
|
|
65
|
+
*/
|
|
66
|
+
function createWebhook(config = {}) {
|
|
67
|
+
return createWebhookHandler(config);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
createClient,
|
|
72
|
+
createOtp,
|
|
73
|
+
createWebhook,
|
|
74
|
+
DEFAULT_BASE_URL,
|
|
75
|
+
// Export webhook constants untuk akses mudah
|
|
76
|
+
WEBHOOK_EVENTS: createWebhookHandler.WEBHOOK_EVENTS,
|
|
77
|
+
PAYLOAD_PARSERS: createWebhookHandler.PAYLOAD_PARSERS
|
|
78
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const createHttpClient = require('../core/http-client');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OTP Plugin untuk Andi WA Gateway
|
|
5
|
+
* Plugin khusus untuk mengirim kode OTP/verifikasi
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function createOtpPlugin(config) {
|
|
9
|
+
const http = createHttpClient(config);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
config,
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate kode OTP random
|
|
16
|
+
* @param {number} [length] - Panjang kode (default: 6)
|
|
17
|
+
* @returns {string} Kode OTP
|
|
18
|
+
*/
|
|
19
|
+
generateCode(length = 6) {
|
|
20
|
+
const digits = '0123456789';
|
|
21
|
+
let code = '';
|
|
22
|
+
for (let i = 0; i < length; i++) {
|
|
23
|
+
code += digits[Math.floor(Math.random() * digits.length)];
|
|
24
|
+
}
|
|
25
|
+
return code;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format pesan OTP (bisa di-override)
|
|
30
|
+
* @param {string} code - Kode OTP
|
|
31
|
+
* @param {string} [appName] - Nama aplikasi
|
|
32
|
+
* @returns {string} Pesan yang diformat
|
|
33
|
+
*/
|
|
34
|
+
formatMessage(code, appName = 'Aplikasi') {
|
|
35
|
+
return `Kode verifikasi ${appName}: ${code}\n\n` +
|
|
36
|
+
`Jangan berikan kode ini kepada siapapun.\n` +
|
|
37
|
+
`Kode berlaku 5 menit.`;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Kirim OTP ke nomor WhatsApp
|
|
42
|
+
* @param {string} number - Nomor tujuan
|
|
43
|
+
* @param {Object} [options]
|
|
44
|
+
* @param {string} [options.code] - Kode OTP (auto-generate jika tidak diisi)
|
|
45
|
+
* @param {string} [options.appName] - Nama aplikasi untuk pesan
|
|
46
|
+
* @param {string} [options.sessionId] - Override session ID
|
|
47
|
+
* @returns {Promise<Object>} Response dari API + kode yang dikirim
|
|
48
|
+
*/
|
|
49
|
+
async send(number, options = {}) {
|
|
50
|
+
const code = options.code || this.generateCode();
|
|
51
|
+
const message = this.formatMessage(code, options.appName);
|
|
52
|
+
const sessionId = options.sessionId || config.sessionId;
|
|
53
|
+
|
|
54
|
+
const response = await http.sendMessage(sessionId, number, message);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...response,
|
|
58
|
+
code, // Return kode untuk disimpan di database
|
|
59
|
+
number: formatNumber(number)
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Kirim OTP dengan template custom
|
|
65
|
+
* @param {string} number - Nomor tujuan
|
|
66
|
+
* @param {string} template - Template pesan dengan placeholder {code}
|
|
67
|
+
* @param {Object} [options]
|
|
68
|
+
* @param {string} [options.code] - Kode OTP
|
|
69
|
+
* @param {string} [options.sessionId] - Override session ID
|
|
70
|
+
*/
|
|
71
|
+
async sendWithTemplate(number, template, options = {}) {
|
|
72
|
+
const code = options.code || this.generateCode();
|
|
73
|
+
const message = template.replace(/{code}/g, code);
|
|
74
|
+
const sessionId = options.sessionId || config.sessionId;
|
|
75
|
+
|
|
76
|
+
const response = await http.sendMessage(sessionId, number, message);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...response,
|
|
80
|
+
code,
|
|
81
|
+
number: formatNumber(number)
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify kode OTP (logic di sisi client/database)
|
|
87
|
+
* @param {string} inputCode - Kode yang diinput user
|
|
88
|
+
* @param {string} storedCode - Kode yang tersimpan di database
|
|
89
|
+
* @returns {boolean} Valid atau tidak
|
|
90
|
+
*/
|
|
91
|
+
verify(inputCode, storedCode) {
|
|
92
|
+
return String(inputCode) === String(storedCode);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatNumber(number) {
|
|
98
|
+
let clean = String(number).replace(/[\s\+-]/g, '');
|
|
99
|
+
if (clean.startsWith('0')) {
|
|
100
|
+
clean = '62' + clean.substring(1);
|
|
101
|
+
}
|
|
102
|
+
return clean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = createOtpPlugin;
|
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Handler untuk Andi WA Gateway
|
|
3
|
+
*
|
|
4
|
+
* Menangani 35 Event Webhook dengan 63+ Field Payload
|
|
5
|
+
* Kategori Event:
|
|
6
|
+
* - Pesan & Media (8 event)
|
|
7
|
+
* - Kontak & Profil (3 event)
|
|
8
|
+
* - Chat & Grup (11 event)
|
|
9
|
+
* - Notifikasi & Status (3 event)
|
|
10
|
+
* - Session & Koneksi (6 event)
|
|
11
|
+
* - Blocklist & Broadcast (2 event)
|
|
12
|
+
* - Newsletter & Community (2 event)
|
|
13
|
+
* - Payment (1 event)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// WEBHOOK EVENT CONSTANTS (35 Events)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const WEBHOOK_EVENTS = {
|
|
21
|
+
// Pesan & Media (8 events)
|
|
22
|
+
MESSAGES_UPSERT: 'messages.upsert',
|
|
23
|
+
MESSAGE_STATUS: 'message.status',
|
|
24
|
+
MESSAGE_REACTION: 'message.reaction',
|
|
25
|
+
MESSAGE_POLL_VOTE: 'message.poll_vote',
|
|
26
|
+
MESSAGE_EDIT: 'message.edit',
|
|
27
|
+
MESSAGE_REVOKE: 'message.revoke',
|
|
28
|
+
MESSAGE_DELETE: 'message.delete',
|
|
29
|
+
MESSAGE_HISTORY_SYNC: 'message.history_sync',
|
|
30
|
+
|
|
31
|
+
// Kontak & Profil (3 events)
|
|
32
|
+
CONTACT_UPSERT: 'contact.upsert',
|
|
33
|
+
CONTACT_UPDATE: 'contact.update',
|
|
34
|
+
PRESENCE_UPDATE: 'presence.update',
|
|
35
|
+
|
|
36
|
+
// Chat & Grup (11 events)
|
|
37
|
+
CHAT_UPSERT: 'chat.upsert',
|
|
38
|
+
CHAT_UPDATE: 'chat.update',
|
|
39
|
+
CHAT_PIN: 'chat.pin',
|
|
40
|
+
CHAT_UNPIN: 'chat.unpin',
|
|
41
|
+
CHAT_ARCHIVE: 'chat.archive',
|
|
42
|
+
CHAT_UNARCHIVE: 'chat.unarchive',
|
|
43
|
+
GROUP_PARTICIPANTS: 'group.participants',
|
|
44
|
+
GROUP_UPDATE: 'group.update',
|
|
45
|
+
GROUP_MEMBERSHIP_REQUEST: 'group.membership_request',
|
|
46
|
+
GROUP_JOIN: 'group.join',
|
|
47
|
+
GROUP_LEAVE: 'group.leave',
|
|
48
|
+
|
|
49
|
+
// Notifikasi & Status (3 events)
|
|
50
|
+
CALL_NOTIFICATION: 'call.notification',
|
|
51
|
+
STATUS_UPDATE: 'status.update',
|
|
52
|
+
LABELS_UPDATE: 'labels.update',
|
|
53
|
+
|
|
54
|
+
// Session & Koneksi (6 events)
|
|
55
|
+
CONNECTION_UPDATE: 'connection.update',
|
|
56
|
+
CREDS_UPDATE: 'creds.update',
|
|
57
|
+
QR_GENERATED: 'qr.generated',
|
|
58
|
+
SESSION_LOGOUT: 'session.logout',
|
|
59
|
+
SYNC_REQUIRED: 'sync.required',
|
|
60
|
+
MESSAGE_MEDIA_DOWNLOAD: 'message.media_download',
|
|
61
|
+
|
|
62
|
+
// Blocklist & Broadcast (2 events)
|
|
63
|
+
BLOCKLIST_UPDATE: 'blocklist.update',
|
|
64
|
+
BROADCAST_UPDATE: 'broadcast.update',
|
|
65
|
+
|
|
66
|
+
// Newsletter & Community (2 events)
|
|
67
|
+
NEWSLETTER_UPDATE: 'newsletter.update',
|
|
68
|
+
COMMUNITY_UPDATE: 'community.update',
|
|
69
|
+
|
|
70
|
+
// Payment (1 event)
|
|
71
|
+
PAYMENT_UPDATE: 'payment.update'
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// PAYLOAD PARSER - 63+ Field Support
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse payload messages.upsert (Pesan Masuk)
|
|
80
|
+
* Field yang tersedia: 25+ field
|
|
81
|
+
*/
|
|
82
|
+
function parseMessagePayload(data) {
|
|
83
|
+
return {
|
|
84
|
+
// Identitas
|
|
85
|
+
sessionId: data.sessionId,
|
|
86
|
+
device_id: data.device_id,
|
|
87
|
+
device: data.device,
|
|
88
|
+
|
|
89
|
+
// Pengirim
|
|
90
|
+
from: data.from,
|
|
91
|
+
participant: data.participant,
|
|
92
|
+
push_name: data.push_name,
|
|
93
|
+
|
|
94
|
+
// Pesan
|
|
95
|
+
message_id: data.message_id,
|
|
96
|
+
message_type: data.message_type,
|
|
97
|
+
message: data.message,
|
|
98
|
+
|
|
99
|
+
// Chat Info
|
|
100
|
+
chat_type: data.chat_type,
|
|
101
|
+
is_group: data.is_group,
|
|
102
|
+
is_me: data.is_me,
|
|
103
|
+
|
|
104
|
+
// Status Pesan
|
|
105
|
+
is_view_once: data.is_view_once,
|
|
106
|
+
is_ephemeral: data.is_ephemeral,
|
|
107
|
+
is_business: data.is_business,
|
|
108
|
+
is_marketing_keyword: data.is_marketing_keyword,
|
|
109
|
+
is_mentioned: data.is_mentioned,
|
|
110
|
+
|
|
111
|
+
// Media/File
|
|
112
|
+
file: data.file, // { type, mimetype, fileName, fileSize, caption }
|
|
113
|
+
|
|
114
|
+
// Mentions & Quotes
|
|
115
|
+
mentioned_jids: data.mentioned_jids || [],
|
|
116
|
+
quoted_message: data.quoted_message,
|
|
117
|
+
|
|
118
|
+
// Timestamp
|
|
119
|
+
timestamp: data.timestamp,
|
|
120
|
+
datetime: data.timestamp ? new Date(data.timestamp * 1000) : null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse payload message.status (Status Pesan)
|
|
126
|
+
* Field: message_id, from, status, timestamp
|
|
127
|
+
*/
|
|
128
|
+
function parseMessageStatusPayload(data) {
|
|
129
|
+
return {
|
|
130
|
+
message_id: data.message_id,
|
|
131
|
+
from: data.from,
|
|
132
|
+
status: data.status, // 'pending', 'sent', 'delivered', 'read', 'played'
|
|
133
|
+
timestamp: data.timestamp,
|
|
134
|
+
datetime: data.timestamp ? new Date(data.timestamp) : null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse payload presence.update (Status Online/Typing)
|
|
140
|
+
*/
|
|
141
|
+
function parsePresencePayload(data) {
|
|
142
|
+
return {
|
|
143
|
+
id: data.id,
|
|
144
|
+
presence: data.presence?.lastKnownPresence, // 'composing', 'recording', 'available', 'unavailable'
|
|
145
|
+
timestamp: data.timestamp
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse payload group.participants (Perubahan Anggota Grup)
|
|
151
|
+
*/
|
|
152
|
+
function parseGroupParticipantsPayload(data) {
|
|
153
|
+
return {
|
|
154
|
+
group_id: data.group_id,
|
|
155
|
+
action: data.action, // 'add', 'remove', 'promote', 'demote'
|
|
156
|
+
participants: data.participants || [],
|
|
157
|
+
by: data.by, // Siapa yang melakukan aksi
|
|
158
|
+
timestamp: data.timestamp
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse payload group.update (Update Info Grup)
|
|
164
|
+
*/
|
|
165
|
+
function parseGroupUpdatePayload(data) {
|
|
166
|
+
return {
|
|
167
|
+
group_id: data.group_id,
|
|
168
|
+
subject: data.subject, // Nama grup baru
|
|
169
|
+
description: data.description,
|
|
170
|
+
owner: data.owner,
|
|
171
|
+
participants_count: data.participants_count,
|
|
172
|
+
timestamp: data.timestamp
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse payload connection.update (Status Koneksi)
|
|
178
|
+
*/
|
|
179
|
+
function parseConnectionPayload(data) {
|
|
180
|
+
return {
|
|
181
|
+
connection: data.connection, // 'connecting', 'open', 'close', 'error'
|
|
182
|
+
isNewLogin: data.isNewLogin,
|
|
183
|
+
qr: data.qr, // QR string jika ada
|
|
184
|
+
receivedPendingNotifications: data.receivedPendingNotifications,
|
|
185
|
+
timestamp: data.timestamp
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse payload payment.update (Pembayaran)
|
|
191
|
+
*/
|
|
192
|
+
function parsePaymentPayload(data) {
|
|
193
|
+
return {
|
|
194
|
+
payment_id: data.payment_id,
|
|
195
|
+
status: data.status, // 'pending', 'completed', 'failed'
|
|
196
|
+
amount: data.amount,
|
|
197
|
+
currency: data.currency,
|
|
198
|
+
sender: data.sender,
|
|
199
|
+
receiver: data.receiver,
|
|
200
|
+
timestamp: data.timestamp
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parse payload call.notification (Panggilan)
|
|
206
|
+
*/
|
|
207
|
+
function parseCallPayload(data) {
|
|
208
|
+
return {
|
|
209
|
+
call_id: data.call_id,
|
|
210
|
+
from: data.from,
|
|
211
|
+
isVideo: data.isVideo,
|
|
212
|
+
isGroupCall: data.isGroupCall,
|
|
213
|
+
status: data.status, // 'offer', 'accept', 'reject', 'timeout'
|
|
214
|
+
timestamp: data.timestamp
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Parse payload contact.update (Update Kontak)
|
|
220
|
+
*/
|
|
221
|
+
function parseContactPayload(data) {
|
|
222
|
+
return {
|
|
223
|
+
id: data.id,
|
|
224
|
+
name: data.name,
|
|
225
|
+
notify: data.notify,
|
|
226
|
+
imgUrl: data.imgUrl,
|
|
227
|
+
status: data.status,
|
|
228
|
+
timestamp: data.timestamp
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// MAIN WEBHOOK HANDLER
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
function createWebhookHandler(config = {}) {
|
|
237
|
+
const handlers = new Map();
|
|
238
|
+
const middlewares = [];
|
|
239
|
+
|
|
240
|
+
const handler = {
|
|
241
|
+
config,
|
|
242
|
+
|
|
243
|
+
// ========================================================================
|
|
244
|
+
// EVENT REGISTRATION
|
|
245
|
+
// ========================================================================
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Register handler untuk event tertentu
|
|
249
|
+
* @param {string} event - Nama event dari WEBHOOK_EVENTS
|
|
250
|
+
* @param {Function} handler - Function handler (parsedPayload, rawPayload) => void
|
|
251
|
+
*/
|
|
252
|
+
on(event, handler) {
|
|
253
|
+
if (!handlers.has(event)) {
|
|
254
|
+
handlers.set(event, []);
|
|
255
|
+
}
|
|
256
|
+
handlers.get(event).push(handler);
|
|
257
|
+
return this; // Chainable
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Register multiple handlers sekaligus
|
|
262
|
+
* @param {Object} handlersMap - { event: handler }
|
|
263
|
+
*/
|
|
264
|
+
onMany(handlersMap) {
|
|
265
|
+
Object.entries(handlersMap).forEach(([event, handler]) => {
|
|
266
|
+
this.on(event, handler);
|
|
267
|
+
});
|
|
268
|
+
return this;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Remove handler untuk event
|
|
273
|
+
*/
|
|
274
|
+
off(event, handlerToRemove) {
|
|
275
|
+
if (handlers.has(event)) {
|
|
276
|
+
const eventHandlers = handlers.get(event);
|
|
277
|
+
const index = eventHandlers.indexOf(handlerToRemove);
|
|
278
|
+
if (index > -1) {
|
|
279
|
+
eventHandlers.splice(index, 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return this;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ========================================================================
|
|
286
|
+
// CORE PROCESSING
|
|
287
|
+
// ========================================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse payload berdasarkan event type
|
|
291
|
+
*/
|
|
292
|
+
parsePayload(event, data) {
|
|
293
|
+
switch (event) {
|
|
294
|
+
case WEBHOOK_EVENTS.MESSAGES_UPSERT:
|
|
295
|
+
return parseMessagePayload(data);
|
|
296
|
+
case WEBHOOK_EVENTS.MESSAGE_STATUS:
|
|
297
|
+
return parseMessageStatusPayload(data);
|
|
298
|
+
case WEBHOOK_EVENTS.PRESENCE_UPDATE:
|
|
299
|
+
return parsePresencePayload(data);
|
|
300
|
+
case WEBHOOK_EVENTS.GROUP_PARTICIPANTS:
|
|
301
|
+
return parseGroupParticipantsPayload(data);
|
|
302
|
+
case WEBHOOK_EVENTS.GROUP_UPDATE:
|
|
303
|
+
return parseGroupUpdatePayload(data);
|
|
304
|
+
case WEBHOOK_EVENTS.CONNECTION_UPDATE:
|
|
305
|
+
return parseConnectionPayload(data);
|
|
306
|
+
case WEBHOOK_EVENTS.PAYMENT_UPDATE:
|
|
307
|
+
return parsePaymentPayload(data);
|
|
308
|
+
case WEBHOOK_EVENTS.CALL_NOTIFICATION:
|
|
309
|
+
return parseCallPayload(data);
|
|
310
|
+
case WEBHOOK_EVENTS.CONTACT_UPSERT:
|
|
311
|
+
case WEBHOOK_EVENTS.CONTACT_UPDATE:
|
|
312
|
+
return parseContactPayload(data);
|
|
313
|
+
default:
|
|
314
|
+
return data; // Return as-is untuk event lain
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Process webhook payload
|
|
320
|
+
*/
|
|
321
|
+
async process(rawPayload) {
|
|
322
|
+
const event = rawPayload.event || WEBHOOK_EVENTS.MESSAGES_UPSERT;
|
|
323
|
+
const data = rawPayload.data || rawPayload;
|
|
324
|
+
|
|
325
|
+
// Parse payload sesuai event type
|
|
326
|
+
const parsedPayload = this.parsePayload(event, data);
|
|
327
|
+
|
|
328
|
+
// Jalankan middlewares
|
|
329
|
+
for (const mw of middlewares) {
|
|
330
|
+
await mw(parsedPayload, rawPayload, event);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Jalankan event handlers
|
|
334
|
+
const eventHandlers = handlers.get(event) || [];
|
|
335
|
+
const results = [];
|
|
336
|
+
|
|
337
|
+
for (const handler of eventHandlers) {
|
|
338
|
+
try {
|
|
339
|
+
const result = await handler(parsedPayload, rawPayload);
|
|
340
|
+
results.push({ success: true, result });
|
|
341
|
+
} catch (err) {
|
|
342
|
+
results.push({ success: false, error: err.message });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
event,
|
|
348
|
+
processed: results.length,
|
|
349
|
+
results,
|
|
350
|
+
parsed: parsedPayload
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// ========================================================================
|
|
355
|
+
// EXPRESS MIDDLEWARE
|
|
356
|
+
// ========================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Express/Fastify middleware
|
|
360
|
+
*/
|
|
361
|
+
middleware() {
|
|
362
|
+
return async (req, res, next) => {
|
|
363
|
+
try {
|
|
364
|
+
// Verify secret jika di-configure
|
|
365
|
+
if (config.secret) {
|
|
366
|
+
const secret = req.headers['x-webhook-secret'];
|
|
367
|
+
if (secret !== config.secret) {
|
|
368
|
+
return res.status(401).json({
|
|
369
|
+
error: 'Invalid webhook secret',
|
|
370
|
+
code: 'UNAUTHORIZED'
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Verify signature jika di-configure
|
|
376
|
+
if (config.verifySignature && config.signatureKey) {
|
|
377
|
+
const signature = req.headers['x-signature'];
|
|
378
|
+
// TODO: Implement signature verification
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const rawPayload = req.body;
|
|
382
|
+
|
|
383
|
+
// Validasi payload
|
|
384
|
+
if (!rawPayload || typeof rawPayload !== 'object') {
|
|
385
|
+
return res.status(400).json({
|
|
386
|
+
error: 'Invalid payload',
|
|
387
|
+
code: 'BAD_REQUEST'
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Process
|
|
392
|
+
const result = await this.process(rawPayload);
|
|
393
|
+
|
|
394
|
+
res.json({
|
|
395
|
+
success: true,
|
|
396
|
+
processed: result.processed,
|
|
397
|
+
event: result.event
|
|
398
|
+
});
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (next) {
|
|
401
|
+
next(err);
|
|
402
|
+
} else {
|
|
403
|
+
res.status(500).json({
|
|
404
|
+
error: err.message,
|
|
405
|
+
code: 'INTERNAL_ERROR'
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Add middleware
|
|
414
|
+
*/
|
|
415
|
+
use(middleware) {
|
|
416
|
+
middlewares.push(middleware);
|
|
417
|
+
return this;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
// ========================================================================
|
|
421
|
+
// CONVENIENCE HANDLERS (Shortcuts)
|
|
422
|
+
// ========================================================================
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Handler untuk pesan masuk (messages.upsert)
|
|
426
|
+
* @param {Function} handler - (message) => void
|
|
427
|
+
*/
|
|
428
|
+
onMessage(handler) {
|
|
429
|
+
this.on(WEBHOOK_EVENTS.MESSAGES_UPSERT, (parsed, raw) => {
|
|
430
|
+
handler(parsed, raw);
|
|
431
|
+
});
|
|
432
|
+
return this;
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Handler untuk pesan teks saja (filter media)
|
|
437
|
+
*/
|
|
438
|
+
onTextMessage(handler) {
|
|
439
|
+
this.on(WEBHOOK_EVENTS.MESSAGES_UPSERT, (parsed, raw) => {
|
|
440
|
+
if (parsed.message_type === 'text' && !parsed.is_me) {
|
|
441
|
+
handler(parsed, raw);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return this;
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Handler untuk media (image, video, audio, document)
|
|
449
|
+
*/
|
|
450
|
+
onMedia(handler) {
|
|
451
|
+
const mediaTypes = ['image', 'video', 'audio', 'document', 'sticker'];
|
|
452
|
+
this.on(WEBHOOK_EVENTS.MESSAGES_UPSERT, (parsed, raw) => {
|
|
453
|
+
if (mediaTypes.includes(parsed.message_type) && !parsed.is_me) {
|
|
454
|
+
handler(parsed, raw);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
return this;
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Handler untuk grup message saja
|
|
462
|
+
*/
|
|
463
|
+
onGroupMessage(handler) {
|
|
464
|
+
this.on(WEBHOOK_EVENTS.MESSAGES_UPSERT, (parsed, raw) => {
|
|
465
|
+
if (parsed.is_group && !parsed.is_me) {
|
|
466
|
+
handler(parsed, raw);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
return this;
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Handler untuk private message saja
|
|
474
|
+
*/
|
|
475
|
+
onPrivateMessage(handler) {
|
|
476
|
+
this.on(WEBHOOK_EVENTS.MESSAGES_UPSERT, (parsed, raw) => {
|
|
477
|
+
if (!parsed.is_group && !parsed.is_me) {
|
|
478
|
+
handler(parsed, raw);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
return this;
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Handler untuk status pesan (sent/delivered/read)
|
|
486
|
+
*/
|
|
487
|
+
onStatusUpdate(handler) {
|
|
488
|
+
this.on(WEBHOOK_EVENTS.MESSAGE_STATUS, handler);
|
|
489
|
+
return this;
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Handler untuk pesan diterima (delivered)
|
|
494
|
+
*/
|
|
495
|
+
onDelivered(handler) {
|
|
496
|
+
this.on(WEBHOOK_EVENTS.MESSAGE_STATUS, (parsed, raw) => {
|
|
497
|
+
if (parsed.status === 'delivered') {
|
|
498
|
+
handler(parsed, raw);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
return this;
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Handler untuk pesan dibaca (read)
|
|
506
|
+
*/
|
|
507
|
+
onRead(handler) {
|
|
508
|
+
this.on(WEBHOOK_EVENTS.MESSAGE_STATUS, (parsed, raw) => {
|
|
509
|
+
if (parsed.status === 'read') {
|
|
510
|
+
handler(parsed, raw);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return this;
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Handler untuk kontak sedang mengetik
|
|
518
|
+
*/
|
|
519
|
+
onTyping(handler) {
|
|
520
|
+
this.on(WEBHOOK_EVENTS.PRESENCE_UPDATE, (parsed, raw) => {
|
|
521
|
+
if (parsed.presence === 'composing') {
|
|
522
|
+
handler(parsed, raw);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return this;
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Handler untuk perubahan grup
|
|
530
|
+
*/
|
|
531
|
+
onGroupUpdate(handler) {
|
|
532
|
+
this.on(WEBHOOK_EVENTS.GROUP_UPDATE, handler);
|
|
533
|
+
return this;
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Handler untuk member join/leave grup
|
|
538
|
+
*/
|
|
539
|
+
onGroupParticipants(handler) {
|
|
540
|
+
this.on(WEBHOOK_EVENTS.GROUP_PARTICIPANTS, handler);
|
|
541
|
+
return this;
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Handler untuk perubahan koneksi
|
|
546
|
+
*/
|
|
547
|
+
onConnectionUpdate(handler) {
|
|
548
|
+
this.on(WEBHOOK_EVENTS.CONNECTION_UPDATE, handler);
|
|
549
|
+
return this;
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Handler untuk QR code generated
|
|
554
|
+
*/
|
|
555
|
+
onQR(handler) {
|
|
556
|
+
this.on(WEBHOOK_EVENTS.QR_GENERATED, handler);
|
|
557
|
+
return this;
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Handler untuk incoming call
|
|
562
|
+
*/
|
|
563
|
+
onCall(handler) {
|
|
564
|
+
this.on(WEBHOOK_EVENTS.CALL_NOTIFICATION, handler);
|
|
565
|
+
return this;
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Handler untuk payment update
|
|
570
|
+
*/
|
|
571
|
+
onPayment(handler) {
|
|
572
|
+
this.on(WEBHOOK_EVENTS.PAYMENT_UPDATE, handler);
|
|
573
|
+
return this;
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Handler untuk kontak update
|
|
578
|
+
*/
|
|
579
|
+
onContactUpdate(handler) {
|
|
580
|
+
this.on(WEBHOOK_EVENTS.CONTACT_UPDATE, handler);
|
|
581
|
+
return this;
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
// ========================================================================
|
|
585
|
+
// EXPRESS ROUTER
|
|
586
|
+
// ========================================================================
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Create Express router dengan endpoint siap pakai
|
|
590
|
+
*/
|
|
591
|
+
createRouter() {
|
|
592
|
+
const express = require('express');
|
|
593
|
+
const router = express.Router();
|
|
594
|
+
|
|
595
|
+
router.post('/', this.middleware());
|
|
596
|
+
|
|
597
|
+
// Health check endpoint
|
|
598
|
+
router.get('/health', (req, res) => {
|
|
599
|
+
res.json({ status: 'ok', handler: 'andi-wa-gateway-webhook' });
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
return router;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
return handler;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// EXPORTS
|
|
611
|
+
// ============================================================================
|
|
612
|
+
|
|
613
|
+
module.exports = createWebhookHandler;
|
|
614
|
+
module.exports.WEBHOOK_EVENTS = WEBHOOK_EVENTS;
|
|
615
|
+
module.exports.PAYLOAD_PARSERS = {
|
|
616
|
+
parseMessagePayload,
|
|
617
|
+
parseMessageStatusPayload,
|
|
618
|
+
parsePresencePayload,
|
|
619
|
+
parseGroupParticipantsPayload,
|
|
620
|
+
parseGroupUpdatePayload,
|
|
621
|
+
parseConnectionPayload,
|
|
622
|
+
parsePaymentPayload,
|
|
623
|
+
parseCallPayload,
|
|
624
|
+
parseContactPayload
|
|
625
|
+
};
|