phlo-whatsapp 1.0.0-RC3
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 +43 -0
- package/package.json +20 -0
- package/phlo-whatsapp.js +390 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jordi Boerman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Phlo WhatsApp
|
|
2
|
+
|
|
3
|
+
WhatsApp Web gateway (whatsapp-web.js + Express) for the [Phlo](https://phlo.tech) framework. One process per WhatsApp number; inbound messages reach the app through a secret-protected webhook, outbound messages are sent through a local HTTP bridge.
|
|
4
|
+
|
|
5
|
+
Phlo WhatsApp is the messaging half of the Phlo server layer, next to Phlo Realtime (the WebSocket layer built into the [Phlo Daemon](https://github.com/q-ainl/phlo-daemon)) for realtime. The engine's `WhatsApp` resource handles the webhook on the app side; the [Phlo Dashboard](https://github.com/q-ainl/phlo-dashboard) shows the status of every instance across the fleet.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
```js
|
|
9
|
+
require('./phlo-whatsapp.js')('wa1', 8081, '<secret>', 'https://app.example.com/receive/whatsapp/web/wa1')
|
|
10
|
+
```
|
|
11
|
+
Arguments: `(instanceId, port, secret, webhookUrl)`.
|
|
12
|
+
|
|
13
|
+
On first start the instance prints a QR code in the terminal; scan it with the WhatsApp account that this instance should send and receive as. The session is persisted, so this is a one-time step per instance.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
```sh
|
|
17
|
+
npm install # deps: whatsapp-web.js, express, axios, qrcode-terminal
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Production
|
|
21
|
+
|
|
22
|
+
Keep one small config file per instance so the gateway and its webhook are managed in one place:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// config/wa1.js (node-local, keep out of version control)
|
|
26
|
+
require('../whatsapp/phlo-whatsapp.js')('wa1', 8081, 'a-long-random-secret', 'https://app.example.com/receive/whatsapp/web/wa1')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The secret is an inline literal on purpose: the Phlo Dashboard discovers each
|
|
30
|
+
instance and proxies to it by reading `config/wa*.js`, so it must be a plain
|
|
31
|
+
string here, not pulled from an env var the dashboard cannot see. Keep this
|
|
32
|
+
file node-local and out of version control.
|
|
33
|
+
|
|
34
|
+
Run each instance under a process manager, for example pm2:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pm2 start config/wa1.js --name wa1
|
|
38
|
+
pm2 save
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "phlo-whatsapp",
|
|
3
|
+
"version": "1.0.0-RC3",
|
|
4
|
+
"description": "Phlo WhatsApp server",
|
|
5
|
+
"main": "phlo-whatsapp.js",
|
|
6
|
+
"keywords": [],
|
|
7
|
+
"author": "q-ai.nl",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"files": [
|
|
10
|
+
"phlo-whatsapp.js",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"axios": "^1.13.0",
|
|
16
|
+
"express": "^5.1.0",
|
|
17
|
+
"qrcode-terminal": "^0.12.0",
|
|
18
|
+
"whatsapp-web.js": "^1.34.6"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/phlo-whatsapp.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
module.exports = (sessionId, port, secret, webhook = null) => {
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
const express = require('express')
|
|
5
|
+
const qrcode = require('qrcode-terminal')
|
|
6
|
+
const QRCodeLib = require('qrcode-terminal/vendor/QRCode')
|
|
7
|
+
const QRErrorCorrectLevel = require('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel')
|
|
8
|
+
const {
|
|
9
|
+
Client,
|
|
10
|
+
LocalAuth,
|
|
11
|
+
MessageMedia,
|
|
12
|
+
Location,
|
|
13
|
+
Poll,
|
|
14
|
+
} = require('whatsapp-web.js')
|
|
15
|
+
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use((req, res, next) => req.headers.secret === secret ? next() : res.status(401).json({ error: 'Unauthorized' }))
|
|
18
|
+
app.use(express.json({ limit: '96mb' }))
|
|
19
|
+
|
|
20
|
+
const client = new Client({
|
|
21
|
+
authStrategy: new LocalAuth({
|
|
22
|
+
clientId: sessionId,
|
|
23
|
+
dataPath: path.join(__dirname, '.wwebjs_auth'),
|
|
24
|
+
}),
|
|
25
|
+
puppeteer: {
|
|
26
|
+
headless: true,
|
|
27
|
+
executablePath: '/usr/bin/google-chrome',
|
|
28
|
+
args: [
|
|
29
|
+
'--no-sandbox',
|
|
30
|
+
'--disable-setuid-sandbox',
|
|
31
|
+
'--disable-dev-shm-usage',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
let clientReady = false
|
|
36
|
+
let clientState = 'connecting'
|
|
37
|
+
let latestQr = null
|
|
38
|
+
const startedAt = Date.now()
|
|
39
|
+
|
|
40
|
+
const qrToSvgDataUrl = qrString => {
|
|
41
|
+
const qr = new QRCodeLib(-1, QRErrorCorrectLevel.L)
|
|
42
|
+
qr.addData(qrString)
|
|
43
|
+
qr.make()
|
|
44
|
+
const n = qr.getModuleCount()
|
|
45
|
+
const cell = 8
|
|
46
|
+
const margin = 3
|
|
47
|
+
const size = (n + margin * 2) * cell
|
|
48
|
+
let rects = ''
|
|
49
|
+
for (let r = 0; r < n; r++) {
|
|
50
|
+
for (let c = 0; c < n; c++) {
|
|
51
|
+
if (qr.isDark(r, c)) {
|
|
52
|
+
const x = (c + margin) * cell
|
|
53
|
+
const y = (r + margin) * cell
|
|
54
|
+
rects += `<rect x="${x}" y="${y}" width="${cell}" height="${cell}"/>`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" fill="white"/><g fill="black">${rects}</g></svg>`
|
|
59
|
+
return 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parseDataUrl = (value, fallbackMime) => {
|
|
63
|
+
if (!value || typeof value !== 'string') return null
|
|
64
|
+
const match = value.match(/^data:([^;]+);base64,(.*)$/s)
|
|
65
|
+
if (match) return { mimetype: match[1], data: match[2] }
|
|
66
|
+
return { mimetype: fallbackMime, data: value }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const toMessageMedia = (value, filename, fallbackMime) => {
|
|
70
|
+
const parsed = parseDataUrl(value, fallbackMime)
|
|
71
|
+
if (!parsed) throw new Error('Invalid media payload')
|
|
72
|
+
return new MessageMedia(parsed.mimetype || 'application/octet-stream', parsed.data, filename)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const inferAudioMime = value => {
|
|
76
|
+
const parsed = parseDataUrl(value)
|
|
77
|
+
return parsed?.mimetype || 'audio/ogg; codecs=opus'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const toLocationText = msg => {
|
|
81
|
+
const lat = msg.location?.latitude ?? msg.lat ?? null
|
|
82
|
+
const lng = msg.location?.longitude ?? msg.lng ?? null
|
|
83
|
+
return lat != null && lng != null ? `${lat},${lng}` : null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const getChatInfo = async msg => {
|
|
87
|
+
try {
|
|
88
|
+
const chat = await msg.getChat()
|
|
89
|
+
return { name: chat?.name || null, isGroup: chat?.isGroup || false }
|
|
90
|
+
} catch {
|
|
91
|
+
return { name: null, isGroup: false }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getContactInfo = async msg => {
|
|
96
|
+
try {
|
|
97
|
+
const contact = await msg.getContact()
|
|
98
|
+
return {
|
|
99
|
+
name: contact?.pushname || contact?.name || null,
|
|
100
|
+
number: contact?.number || null,
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
return { name: null, number: null }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const getContactById = async id => {
|
|
108
|
+
if (!id) return { name: null, number: null }
|
|
109
|
+
try {
|
|
110
|
+
const contact = await client.getContactById(id)
|
|
111
|
+
return {
|
|
112
|
+
name: contact?.pushname || contact?.name || null,
|
|
113
|
+
number: contact?.number || null,
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
return { name: null, number: null }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const normalizeMessage = async msg => {
|
|
121
|
+
const media = msg.hasMedia ? await msg.downloadMedia().catch(() => null) : null
|
|
122
|
+
const chatInfo = await getChatInfo(msg)
|
|
123
|
+
const isGroup = chatInfo.isGroup || msg.from?.endsWith('@g.us')
|
|
124
|
+
const senderContact = await getContactInfo(msg)
|
|
125
|
+
const chatContact = msg.fromMe && !isGroup ? await getContactById(msg.to) : senderContact
|
|
126
|
+
const chatNumber = chatContact.number || null
|
|
127
|
+
const chatId = isGroup ? msg.from : (chatNumber ? `${chatNumber}@c.us` : (msg.fromMe ? msg.to : msg.from))
|
|
128
|
+
const normalizeJid = jid => String(jid || '').replace(/:\d+@/, '@')
|
|
129
|
+
const ownJid = normalizeJid(client.info?.wid?._serialized || client.info?.wid?.user || '')
|
|
130
|
+
const selfMessage = !isGroup && !!msg.fromMe && !!ownJid && normalizeJid(msg.to).startsWith(ownJid.split('@')[0])
|
|
131
|
+
return {
|
|
132
|
+
id: msg.id?._serialized || msg.id || null,
|
|
133
|
+
chat: chatId,
|
|
134
|
+
chatName: chatInfo.name,
|
|
135
|
+
from: msg.author || msg.from,
|
|
136
|
+
fromName: senderContact.name,
|
|
137
|
+
fromNumber: senderContact.number ? `+${senderContact.number}` : null,
|
|
138
|
+
isGroup,
|
|
139
|
+
to: msg.to || null,
|
|
140
|
+
fromMe: !!msg.fromMe,
|
|
141
|
+
selfMessage,
|
|
142
|
+
timestamp: msg.timestamp || null,
|
|
143
|
+
type: msg.type,
|
|
144
|
+
media: media ? {
|
|
145
|
+
mime: media.mimetype || null,
|
|
146
|
+
content: media.data || null,
|
|
147
|
+
filename: media.filename || null,
|
|
148
|
+
} : (msg.type === 'location' ? toLocationText(msg) : null),
|
|
149
|
+
text: msg.body || msg.caption || null,
|
|
150
|
+
isForwarded: !!msg.isForwarded,
|
|
151
|
+
isViewOnce: !!msg.isViewOnce,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const shortenMediaContent = content => `${content.slice(0, 9)}.. - ${content.length}b`
|
|
156
|
+
|
|
157
|
+
const logMessage = msg => {
|
|
158
|
+
const logMsg = structuredClone(msg)
|
|
159
|
+
if (logMsg.media?.content) logMsg.media.content = shortenMediaContent(logMsg.media.content)
|
|
160
|
+
if (logMsg.quotedMsg?.media?.content) logMsg.quotedMsg.media.content = shortenMediaContent(logMsg.quotedMsg.media.content)
|
|
161
|
+
console.log('')
|
|
162
|
+
console.log(logMsg)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const asyncRoute = handler => async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
await handler(req, res)
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(error)
|
|
170
|
+
if (!res.headersSent) res.status(500).json({ error: error.message || 'Internal Server Error' })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
client.on('qr', qr => {
|
|
175
|
+
console.log(`\nScan QR for session "${sessionId}"`)
|
|
176
|
+
qrcode.generate(qr, { small: true })
|
|
177
|
+
clientState = 'qr'
|
|
178
|
+
latestQr = qrToSvgDataUrl(qr)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
client.on('ready', () => {
|
|
182
|
+
clientReady = true
|
|
183
|
+
clientState = 'ready'
|
|
184
|
+
latestQr = null
|
|
185
|
+
console.log(`\nWhatsApp client "${sessionId}" ready`)
|
|
186
|
+
if (webhook) console.log(`\nWebhook active: ${webhook}`)
|
|
187
|
+
})
|
|
188
|
+
client.on('authenticated', () => console.log(`\nWhatsApp client "${sessionId}" authenticated`))
|
|
189
|
+
client.on('auth_failure', message => {
|
|
190
|
+
clientState = 'disconnected'
|
|
191
|
+
latestQr = null
|
|
192
|
+
console.error(`\nWhatsApp auth failure "${sessionId}": ${message}`)
|
|
193
|
+
})
|
|
194
|
+
client.on('disconnected', reason => {
|
|
195
|
+
clientReady = false
|
|
196
|
+
clientState = 'disconnected'
|
|
197
|
+
latestQr = null
|
|
198
|
+
console.error(`\nWhatsApp client "${sessionId}" disconnected: ${reason}`)
|
|
199
|
+
})
|
|
200
|
+
client.on('message_create', async data => {
|
|
201
|
+
const id = data.id?._serialized || data.id || '-'
|
|
202
|
+
const from = data.author || data.from || '-'
|
|
203
|
+
const to = data.to || '-'
|
|
204
|
+
const text = (data.body || data.caption || '').replace(/\s+/g, ' ').trim().slice(0, 120)
|
|
205
|
+
console.log(`\nmessage_create: ${data.type || 'unknown'} fromMe=${!!data.fromMe} from=${from} to=${to} id=${id}`)
|
|
206
|
+
text && console.log(text)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
app.get('/status', (req, res) => {
|
|
210
|
+
res.json({ ok: true, status: clientState })
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
app.get('/health', (req, res) => {
|
|
214
|
+
res.json({ ok: true, sessionId, status: clientState, ready: clientReady, webhook: !!webhook, uptime: Math.round((Date.now() - startedAt) / 1000) })
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
app.get('/qr', (req, res) => {
|
|
218
|
+
res.json({ ok: true, status: clientState, qr: latestQr })
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
app.use((req, res, next) => {
|
|
222
|
+
if (!clientReady) return res.status(503).json({ error: 'WhatsApp client not ready' })
|
|
223
|
+
next()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const postWebhook = async payload => {
|
|
227
|
+
try {
|
|
228
|
+
const res = await axios.post(webhook, payload, { headers: { secret } })
|
|
229
|
+
console.log(`\nwebhook: ${res.status} ${res.statusText || 'OK'} id=${payload.id || '-'}`)
|
|
230
|
+
if (res.data != null) console.log(res.data)
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const status = error.response?.status || '-'
|
|
233
|
+
const body = typeof error.response?.data === 'string' ? error.response.data : JSON.stringify(error.response?.data || {})
|
|
234
|
+
console.error(`\nwebhook error: ${status} id=${payload.id || '-'}`)
|
|
235
|
+
body && console.error(body)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (webhook) {
|
|
240
|
+
client.on('message_create', async data => {
|
|
241
|
+
if (data.from === 'status@broadcast' || data.to === 'status@broadcast') {
|
|
242
|
+
console.log(`\nskipped status@broadcast from=${data.from}`)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
const msg = await normalizeMessage(data)
|
|
246
|
+
if (msg.selfMessage) {
|
|
247
|
+
console.log(`\nskipped self-message id=${msg.id || '-'}`)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (data.hasQuotedMsg) {
|
|
251
|
+
const quoted = await data.getQuotedMessage().catch(() => null)
|
|
252
|
+
msg.quotedMsg = quoted ? await normalizeMessage(quoted) : null
|
|
253
|
+
} else {
|
|
254
|
+
msg.quotedMsg = null
|
|
255
|
+
}
|
|
256
|
+
logMessage(msg)
|
|
257
|
+
await postWebhook(msg)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
client.on('message_ack', async (msg, ack) => {
|
|
261
|
+
if (msg.from === 'status@broadcast' || msg.to === 'status@broadcast') return
|
|
262
|
+
const ackLabels = { 1: 'sent', 2: 'delivered', 3: 'read', 4: 'played' }
|
|
263
|
+
const label = ackLabels[ack] || `ack_${ack}`
|
|
264
|
+
console.log(`\nmessage_ack: ${label} id=${msg.id?._serialized || msg.id} to=${msg.to}`)
|
|
265
|
+
await postWebhook({
|
|
266
|
+
id: msg.id?._serialized || msg.id || null,
|
|
267
|
+
type: 'ack',
|
|
268
|
+
ack: ack,
|
|
269
|
+
ackLabel: label,
|
|
270
|
+
chat: msg.to || msg.from,
|
|
271
|
+
fromMe: !!msg.fromMe,
|
|
272
|
+
timestamp: msg.timestamp || null,
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
app.post('/disconnect', asyncRoute(async (req, res) => {
|
|
278
|
+
console.log(`\ndisconnect: ${sessionId}`)
|
|
279
|
+
await client.logout()
|
|
280
|
+
clientState = 'disconnected'
|
|
281
|
+
clientReady = false
|
|
282
|
+
latestQr = null
|
|
283
|
+
res.json({ ok: true })
|
|
284
|
+
}))
|
|
285
|
+
|
|
286
|
+
app.post('/read', asyncRoute(async (req, res) => {
|
|
287
|
+
const { chat } = req.body
|
|
288
|
+
const target = await client.getChatById(chat)
|
|
289
|
+
await target.sendSeen()
|
|
290
|
+
console.log(`\nread: ${chat}`)
|
|
291
|
+
res.send('ok')
|
|
292
|
+
}))
|
|
293
|
+
|
|
294
|
+
app.post('/reaction', asyncRoute(async (req, res) => {
|
|
295
|
+
const { msg, emoji } = req.body
|
|
296
|
+
const target = await client.getMessageById(msg)
|
|
297
|
+
if (!target) throw new Error(`Message not found: ${msg}`)
|
|
298
|
+
await target.react(emoji)
|
|
299
|
+
console.log(`\nreaction: ${msg} - ${emoji}`)
|
|
300
|
+
res.send('ok')
|
|
301
|
+
}))
|
|
302
|
+
|
|
303
|
+
app.post('/text', asyncRoute(async (req, res) => {
|
|
304
|
+
const { to, text } = req.body
|
|
305
|
+
await client.sendMessage(to, text)
|
|
306
|
+
console.log(`\ntext: ${to}\n${text}`)
|
|
307
|
+
res.send('ok')
|
|
308
|
+
}))
|
|
309
|
+
|
|
310
|
+
app.post('/image', asyncRoute(async (req, res) => {
|
|
311
|
+
const { to, filename, image } = req.body
|
|
312
|
+
const text = req.body.text || ''
|
|
313
|
+
const media = toMessageMedia(image, filename, 'image/jpeg')
|
|
314
|
+
await client.sendMessage(to, media, { caption: text })
|
|
315
|
+
console.log(`\nimage: ${to} - ${filename} (${image.length}b)\n${text}`)
|
|
316
|
+
res.send('ok')
|
|
317
|
+
}))
|
|
318
|
+
|
|
319
|
+
app.post('/location', asyncRoute(async (req, res) => {
|
|
320
|
+
const { to, lat, lon, text } = req.body
|
|
321
|
+
const address = req.body.address || ''
|
|
322
|
+
const url = req.body.url || ''
|
|
323
|
+
const description = [text, address, url].filter(Boolean).join('\n')
|
|
324
|
+
await client.sendMessage(to, new Location(lat, lon, description))
|
|
325
|
+
console.log(`\nlocation: ${lat},${lon}\n${description}`)
|
|
326
|
+
res.send('ok')
|
|
327
|
+
}))
|
|
328
|
+
|
|
329
|
+
app.post('/document', asyncRoute(async (req, res) => {
|
|
330
|
+
const { to, filename, document } = req.body
|
|
331
|
+
const text = req.body.text || ''
|
|
332
|
+
const media = toMessageMedia(document, filename, 'application/octet-stream')
|
|
333
|
+
await client.sendMessage(to, media, {
|
|
334
|
+
caption: text,
|
|
335
|
+
sendMediaAsDocument: true,
|
|
336
|
+
})
|
|
337
|
+
console.log(`\ndocument: ${filename} (${document.length}b)\n${text}`)
|
|
338
|
+
res.send('ok')
|
|
339
|
+
}))
|
|
340
|
+
|
|
341
|
+
app.post('/audio', asyncRoute(async (req, res) => {
|
|
342
|
+
const { to, audio } = req.body
|
|
343
|
+
const media = toMessageMedia(audio, 'audio', inferAudioMime(audio))
|
|
344
|
+
await client.sendMessage(to, media)
|
|
345
|
+
console.log(`\naudio: ${to} ${audio.length}b`)
|
|
346
|
+
res.send('ok')
|
|
347
|
+
}))
|
|
348
|
+
|
|
349
|
+
app.post('/voice', asyncRoute(async (req, res) => {
|
|
350
|
+
const { to, audio } = req.body
|
|
351
|
+
const media = toMessageMedia(audio, 'voice', inferAudioMime(audio))
|
|
352
|
+
await client.sendMessage(to, media, {
|
|
353
|
+
sendAudioAsVoice: true,
|
|
354
|
+
})
|
|
355
|
+
console.log(`\nvoice: ${to} ${audio.length}b`)
|
|
356
|
+
res.send('ok')
|
|
357
|
+
}))
|
|
358
|
+
|
|
359
|
+
app.post('/poll', asyncRoute(async (req, res) => {
|
|
360
|
+
const { to, name, options } = req.body
|
|
361
|
+
const multi = !!+req.body.multi
|
|
362
|
+
await client.sendMessage(to, new Poll(name, options, { allowMultipleAnswers: multi }))
|
|
363
|
+
console.log(`\npoll: ${to} ${multi}\n${name}`)
|
|
364
|
+
console.log(options)
|
|
365
|
+
res.send('ok')
|
|
366
|
+
}))
|
|
367
|
+
|
|
368
|
+
app.post('/typing/start', asyncRoute(async (req, res) => {
|
|
369
|
+
const { to } = req.body
|
|
370
|
+
const chat = await client.getChatById(to)
|
|
371
|
+
await chat.sendStateTyping()
|
|
372
|
+
console.log(`\ntyping/start: ${to}`)
|
|
373
|
+
res.send('ok')
|
|
374
|
+
}))
|
|
375
|
+
|
|
376
|
+
app.post('/typing/stop', asyncRoute(async (req, res) => {
|
|
377
|
+
const { to } = req.body
|
|
378
|
+
const chat = await client.getChatById(to)
|
|
379
|
+
await chat.clearState()
|
|
380
|
+
console.log(`\ntyping/stop: ${to}`)
|
|
381
|
+
res.send('ok')
|
|
382
|
+
}))
|
|
383
|
+
|
|
384
|
+
app.listen(port, '127.0.0.1', () => console.log(`\nServer "${sessionId}" running on port ${port}`))
|
|
385
|
+
|
|
386
|
+
client.initialize().catch(error => {
|
|
387
|
+
console.error(error)
|
|
388
|
+
process.exitCode = 1
|
|
389
|
+
})
|
|
390
|
+
}
|