├── .env
├── .env.exemplo
├── .gitignore
├── API.md
├── Dockerfile
├── LICENSE
├── README.md
├── WuzAPI Collection.postman_collection.json
├── docker-compose-swarm.yaml
├── docker-compose.yml
├── frontend
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ ├── manifest.json
│ └── zpro.png
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Footer.tsx
│ │ ├── Navbar.tsx
│ │ └── ProtectedRoute.tsx
│ ├── contexts
│ │ └── AuthContext.tsx
│ ├── index.tsx
│ └── pages
│ │ ├── ApiDocs.tsx
│ │ ├── Dashboard.tsx
│ │ ├── Instances.tsx
│ │ └── Login.tsx
└── tsconfig.json
├── go.mod
├── go.sum
├── handlers.go
├── helpers.go
├── main.go
├── migrations
├── 0001_create_users_table.down.sql
└── 0001_create_users_table.up.sql
├── repository
└── repository.go
├── routes.go
├── static
├── api
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── index.html
│ ├── spec.yml
│ ├── swagger-ui-bundle.js
│ ├── swagger-ui-bundle.js.map
│ ├── swagger-ui-standalone-preset.js
│ ├── swagger-ui-standalone-preset.js.map
│ ├── swagger-ui.css
│ ├── swagger-ui.css.map
│ ├── swagger-ui.js
│ └── swagger-ui.js.map
├── favicon.ico
├── github-markdown-css
│ ├── code-navigation-banner-illo.svg
│ └── github-css.css
├── images
│ └── favicon.png
├── index.html
├── login
│ └── index.html
└── style.css
├── wmiau.go
├── wuzapi.exe
└── wuzapi.service
/.env:
--------------------------------------------------------------------------------
1 | # .env
2 | WUZAPI_ADMIN_TOKEN=1234ABCD
3 | DB_USER=zpro
4 | DB_PASSWORD=password
5 | DB_NAME=zpro
6 | DB_HOST=localhost
7 | DB_PORT=5432
8 | TZ=America/Sao_Paulo
--------------------------------------------------------------------------------
/.env.exemplo:
--------------------------------------------------------------------------------
1 | # .env
2 | WUZAPI_ADMIN_TOKEN=1234ABCD
3 | DB_USER=postgres
4 | DB_PASSWORD=postgres
5 | DB_NAME=wuzapi
6 | DB_HOST=localhost
7 | DB_PORT=5432
8 | TZ=America/Sao_Paulo
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dbdata/
2 | files/
3 | wuzapi
4 | frontend/node_modules
5 | frontend/package-lock.json
6 | .env
7 | .env.example
8 | .tool-versions
9 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | A API suporta dois tipos de autenticação:
4 |
5 | 1. **Token de usuário**: Para endpoints regulares, use o cabeçalho `Token` com o valor do token do usuário
6 | 2. **Token administrativo**: Para endpoints de administração (/admin/*), use o cabeçalho `Authorization` com o valor do token administrativo definido em WUZAPI_ADMIN_TOKEN
7 |
8 | Na primeira execução, o sistema cria automaticamente um usuário "admin" com o token definido na variável de ambiente WUZAPI_ADMIN_TOKEN.
9 |
10 | As chamadas à API devem ser feitas com o tipo de conteúdo JSON, com os parâmetros enviados no corpo da requisição, sempre passando o cabeçalho Token para autenticar a requisição.
11 |
12 | ---
13 |
14 | ## Admin
15 |
16 | Os seguintes endpoints de _admin_ são usados para gerenciar usuários no sistema.
17 |
18 | ## Listar usuários
19 |
20 | Lista todos os usuários cadastrados no sistema.
21 |
22 | Endpoint: _/admin/users_
23 |
24 | Method: **GET**
25 |
26 | ```
27 | curl -s -X GET -H 'Authorization: {{WUZAPI_ADMIN_TOKEN}}' http://localhost:8080/admin/users
28 | ```
29 |
30 | Response:
31 |
32 | ```json
33 | [
34 | {
35 | "id": 1,
36 | "name": "admin",
37 | "token": "H4Zbhwr72PBrtKdTIgS",
38 | "webhook": "https://example.com/webhook",
39 | "jid": "5491155553934@s.whatsapp.net",
40 | "qrcode": "",
41 | "connected": true,
42 | "expiration": 0,
43 | "events": "Message,ReadReceipt"
44 | }
45 | ]
46 | ```
47 |
48 | ## Adicionar usuário
49 |
50 | Adiciona um novo usuário ao sistema.
51 |
52 | Endpoint: _/admin/users_
53 |
54 | Method: **POST**
55 |
56 | ```
57 | curl -s -X POST -H 'Authorization: {{WUZAPI_ADMIN_TOKEN}}' -H 'Content-Type: application/json' --data '{"name":"usuario2","token":"token2","webhook":"https://example.com/webhook2","events":"Message,ReadReceipt"}' http://localhost:8080/admin/users
58 | ```
59 |
60 | Response:
61 |
62 | ```json
63 | {
64 | "id": 2
65 | }
66 | ```
67 |
68 | ## Remover usuário
69 |
70 | Remove um usuário do sistema pelo seu ID.
71 |
72 | Endpoint: _/admin/users/{id}_
73 |
74 | Method: **DELETE**
75 |
76 | ```
77 | curl -s -X DELETE -H 'Authorization: {{WUZAPI_ADMIN_TOKEN}}' http://localhost:8080/admin/users/2
78 | ```
79 |
80 | Response:
81 |
82 | ```json
83 | {
84 | "Details": "User deleted successfully"
85 | }
86 | ```
87 |
88 | ---
89 |
90 | ## Webhook
91 |
92 | The following _webhook_ endpoints are used to get or set the webhook that will be called whenever a message or event is received. Available event types are:
93 |
94 | * Message
95 | * ReadReceipt
96 | * HistorySync
97 | * ChatPresence
98 |
99 |
100 | ## Sets webhook
101 |
102 | Configures the webhook to be called using POST whenever a subscribed event occurs.
103 |
104 | Endpoint: _/webhook_
105 |
106 | Method: **POST**
107 |
108 |
109 | ```
110 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"webhookURL":"https://some.server/webhook"}' http://localhost:8080/webhook
111 | ```
112 | Response:
113 |
114 | ```json
115 | {
116 | "code": 200,
117 | "data": {
118 | "webhook": "https://example.net/webhook"
119 | },
120 | "success": true
121 | }
122 | ```
123 |
124 | ---
125 |
126 | ## Gets webhook
127 |
128 | Retrieves the configured webhook and subscribed events.
129 |
130 | Endpoint: _/webhook_
131 |
132 | Method: **GET**
133 |
134 | ```
135 | curl -s -X GET -H 'Token: 1234ABCD' http://localhost:8080/webhook
136 | ```
137 | Response:
138 | ```json
139 | {
140 | "code": 200,
141 | "data": {
142 | "subscribe": [ "Message" ],
143 | "webhook": "https://example.net/webhook"
144 | },
145 | "success": true
146 | }
147 | ```
148 |
149 | ---
150 |
151 | ## Session
152 |
153 | The following _session_ endpoints are used to start a session to Whatsapp servers in order to send and receive messages
154 |
155 | ## Connect
156 |
157 | Connects to Whatsapp servers. If is there no existing session it will initiate a QR scan that can be retrieved via the [/session/qr](#user-content-gets-qr-code) endpoint.
158 | You can subscribe to different types of messages so they are POSTED to your configured webhook.
159 | Available message types to subscribe to are:
160 |
161 | * Message
162 | * ReadReceipt
163 | * HistorySync
164 | * ChatPresence
165 |
166 | If you set Immediate to false, the action will wait 10 seconds to verify a successful login. If Immediate is not set or set to true, it will return immedialty, but you will have to check shortly after the /session/status as your session might be disconnected shortly after started if the session was terminated previously via the phone/device.
167 |
168 | Endpoint: _/session/connect_
169 |
170 | Method: **POST**
171 |
172 | ```
173 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Subscribe":["Message"],"Immediate":false}' http://localhost:8080/session/connect
174 | ```
175 |
176 | Response:
177 |
178 | ```json
179 | {
180 | "code": 200,
181 | "data": {
182 | "details": "Connected!",
183 | "events": "Message",
184 | "jid": "5491155554444.0:52@s.whatsapp.net",
185 | "webhook": "http://some.site/webhook?token=123456"
186 | },
187 | "success": true
188 | }
189 | ```
190 |
191 | ---
192 |
193 | ## Disconnect
194 |
195 | Disconnects from Whatsapp servers, keeping the session active. This means that if you /session/connect again, it will
196 | reuse the session and won't require a QR code rescan.
197 |
198 | Endpoint: _/session/disconnect_
199 |
200 | Method: **POST**
201 |
202 |
203 | ```
204 | curl -s -X POST -H 'Token: 1234ABCD' http://localhost:8080/session/disconnect
205 | ```
206 |
207 | Response:
208 |
209 | ```json
210 | {
211 | "code": 200,
212 | "data": {
213 | "Details": "Disconnected"
214 | },
215 | "success": true
216 | }
217 | ```
218 |
219 | ---
220 |
221 | ## Logout
222 |
223 | Disconnects from whatsapp websocket *and* finishes the session (so it will be required to scan a QR code the next time a connection is initiated)
224 |
225 | Endpoint: _/session/logout_
226 |
227 | Method: **POST**
228 |
229 | ```
230 | curl -s -X POST -H 'Token: 1234ABCD' http://localhost:8080/session/logout
231 | ```
232 |
233 | Response:
234 |
235 | ```json
236 | {
237 | "code": 200,
238 | "data": {
239 | "Details": "Logged out"
240 | },
241 | "success": true
242 | }
243 |
244 | ```
245 |
246 | ---
247 |
248 | ## Status
249 |
250 | Retrieve status (IsConnected means websocket connection is initiated, IsLoggedIn means QR code was scanned and session is ready to receive/send messages)
251 |
252 | If its not logged in, you can use the [/session/qr](#user-content-gets-qr-code) endpoint to get the QR code to scan
253 |
254 | Endpoint: _/session/status_
255 |
256 | Method: **GET**
257 |
258 | ```
259 | curl -s -H 'Token: 1234ABCD' http://localhost:8080/session/status
260 | ```
261 |
262 | Response:
263 |
264 | ```json
265 | {
266 | "code": 200,
267 | "data": {
268 | "Connected": true,
269 | "LoggedIn": true
270 | },
271 | "success": true
272 | }
273 |
274 | ```
275 |
276 | ---
277 |
278 | ## Gets QR code
279 |
280 | Retrieves QR code, session must be connected to Whatsapp servers and logged in must be false in order for the QR code to be generated. The generated code
281 | will be returned encoded in base64 embedded format.
282 |
283 | Endpoint: _/session/qr_
284 |
285 | Method: **GET**
286 |
287 | ```
288 | curl -s -H 'Token: 1234ABCD' http://localhost:8080/session/qr
289 | ```
290 | Response:
291 | ```json
292 | {
293 | "code": 200,
294 | "data": {
295 | "QRCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEX///8AAABVwtN+AAAEw0lEQVR42uyZ..."
296 | },
297 | "success": true
298 | }
299 | ```
300 |
301 | ---
302 |
303 | ## User
304 |
305 | The following _user_ endpoints are used to gather information about Whatsapp users.
306 |
307 | ## Gets user details
308 |
309 | Gets information for users on Whatsapp
310 |
311 | Endpoint: _/user/info_
312 |
313 | Method: **POST**
314 |
315 | ```
316 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":["5491155554445","5491155554444"]}' http://localhost:8080/user/info
317 | ```
318 |
319 | Response:
320 |
321 | ```json
322 | {
323 | "code": 200,
324 | "data": {
325 | "Users": {
326 | "5491155554445@s.whatsapp.net": {
327 | "Devices": [],
328 | "PictureID": "",
329 | "Status": "",
330 | "VerifiedName": null
331 | },
332 | "5491155554444@s.whatsapp.net": {
333 | "Devices": [
334 | "5491155554444.0:0@s.whatsapp.net",
335 | "5491155554444.0:11@s.whatsapp.net"
336 | ],
337 | "PictureID": "",
338 | "Status": "",
339 | "VerifiedName": {
340 | "Certificate": {
341 | "details": "CP7t782FIRIGc21iOndeshIghUcml4b2NvbQ==",
342 | "signature": "e35Fd320dccNmaBdNw+Yqtz1Q5545XpT9PpSlntqwaXpj1boOrQUnq9TNhYzGtgPWznTjRl7kHEBQ=="
343 | },
344 | "Details": {
345 | "issuer": "smb:wa",
346 | "serial": 23810327841439764000,
347 | "verifiedName": "Great Company"
348 | }
349 | }
350 | }
351 | }
352 | },
353 | "success": true
354 | }
355 | ```
356 |
357 | ---
358 |
359 | ## Checks Users
360 |
361 | Checks if phone numbers are registered as Whatsapp users
362 |
363 | Endpoint: _/user/check_
364 |
365 | Method: **POST**
366 |
367 | ```
368 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":["5491155554445","5491155554444"]}' http://localhost:8080/user/check
369 | ```
370 |
371 | Response:
372 |
373 | ```json
374 | {
375 | "code": 200,
376 | "data": {
377 | "Users": [
378 | {
379 | "IsInWhatsapp": true,
380 | "JID": "5491155554445@s.whatsapp.net",
381 | "Query": "5491155554445",
382 | "VerifiedName": "Company Name"
383 | },
384 | {
385 | "IsInWhatsapp": false,
386 | "JID": "5491155554444@s.whatsapp.net",
387 | "Query": "5491155554444",
388 | "VerifiedName": ""
389 | }
390 | ]
391 | },
392 | "success": true
393 | }
394 | ```
395 |
396 | ---
397 |
398 | ## Gets Avatar
399 |
400 | Gets information about users profile pictures on WhatsApp, either a thumbnail (Preview=true) or full picture.
401 |
402 | Endpoint: _/user/avatar_
403 |
404 | Method: **GET**
405 |
406 | ```
407 | curl -s -X GET -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554445","Preview":true]}' http://localhost:8080/user/avatar
408 | ```
409 |
410 | Response:
411 |
412 | ```json
413 | {
414 | "URL": "https://pps.whatsapp.net/v/t61.24694-24/227295214_112447507729487_4643695328050510566_n.jpg?stp=dst-jpg_s96x96&ccb=11-4&oh=ja432434a91e8f41d86d341bx889c217&oe=543222A4",
415 | "ID": "1645308319",
416 | "Type": "preview",
417 | "DirectPath": "/v/t61.24694-24/227295214_112447507729487_4643695328050510566_n.jpg?stp=dst-jpg_s96x96&ccb=11-4&oh=ja432434a91e8f41d86d341ba889c217&oe=543222A4"
418 | }
419 | ```
420 |
421 | ---
422 |
423 | ## Gets all contacts
424 |
425 | Gets all contacts for the account.
426 |
427 | Endpoint: _/user/contacts_
428 |
429 | Method: **GET**
430 |
431 | ```
432 | curl -s -X GET -H 'Token: 1234ABCD' http://localhost:8080/user/contacts
433 | ```
434 |
435 | Response:
436 |
437 | ```json
438 | {
439 | "code": 200,
440 | "data": {
441 | "5491122223333@s.whatsapp.net": {
442 | "BusinessName": "",
443 | "FirstName": "",
444 | "Found": true,
445 | "FullName": "",
446 | "PushName": "FOP2"
447 | },
448 | "549113334444@s.whatsapp.net": {
449 | "BusinessName": "",
450 | "FirstName": "",
451 | "Found": true,
452 | "FullName": "",
453 | "PushName": "Asternic"
454 | }
455 | }
456 | }
457 | ```
458 |
459 | ---
460 |
461 |
462 | # Chat
463 |
464 | The following _chat_ endpoints are used to send messages or mark them as read or indicating composing/not composing presence. The sample response is listed only once, as it is the
465 | same for all message types.
466 |
467 | ## Send Text Message
468 |
469 | Sends a text message or reply. For replies, ContextInfo data should be completed with the StanzaID (ID of the message we are replying to), and Participant (user JID we are replying to). If ID is
470 | ommited, a random message ID will be generated.
471 |
472 | Endpoint: _/chat/send/text_
473 |
474 | Method: **POST**
475 |
476 | Example sending a new message:
477 |
478 | ```
479 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Body":"Hellow Meow", "Id": "90B2F8B13FAC8A9CF6B06E99C7834DC5"}' http://localhost:8080/chat/send/text
480 | ```
481 | Example replying to some message:
482 |
483 | ```
484 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Body":"Ditto","ContextInfo":{"StanzaId":"AA3DSE28UDJES3","Participant":"5491155553935@s.whatsapp.net"}}' http://localhost:8080/chat/send/text
485 | ```
486 |
487 | Response:
488 |
489 | ```json
490 | {
491 | "code": 200,
492 | "data": {
493 | "Details": "Sent",
494 | "Id": "90B2F8B13FAC8A9CF6B06E99C7834DC5",
495 | "Timestamp": "2022-04-20T12:49:08-03:00"
496 | },
497 | "success": true
498 | }
499 | ```
500 |
501 | ---
502 |
503 | ## Send Template Message
504 |
505 | Sends a template message or reply. Template messages can contain call to action buttons: up to three quick replies, call button, and link button.
506 |
507 | Endpoint: _/chat/send/template_
508 |
509 | Method: **POST**
510 |
511 |
512 | ```
513 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Content":"Template content","Footer":"Some footer text","Buttons":[{"DisplayText":"Yes","Type":"quickreply"},{"DisplayText":"No","Type":"quickreply"},{"DisplayText":"Visit Site","Type":"url","Url":"https://www.fop2.com"},{"DisplayText":"Llamame","Type":"call","PhoneNumber":"1155554444"}]}' http://localhost:8080/chat/send/template
514 | ```
515 |
516 | ---
517 |
518 | ## Send Audio Message
519 |
520 | Sends an Audio message. Audio must be in Opus format and base64 encoded in embedded format.
521 |
522 | Endpoint: _/chat/send/audio_
523 |
524 | Method: **POST**
525 |
526 |
527 | ```
528 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Audio":"data:audio/ogg;base64,T2dnUw..."}' http://localhost:8080/chat/send/audio
529 | ```
530 |
531 | ## Send Image Message
532 |
533 | Sends an Image message. Image must be in png or jpeg and base64 encoded in embedded format. You can optionally specify a text Caption
534 |
535 | Endpoint: _/chat/send/image_
536 |
537 | Method: **POST**
538 |
539 |
540 | ```
541 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Caption":"Look at this", "Image":"data:image/jpeg;base64,iVBORw0KGgoAAAANSU..."}' http://localhost:8080/chat/send/image
542 | ```
543 |
544 | ---
545 |
546 | ## Send Document Message
547 |
548 | Sends a Document message. Any mime type can be attached. A FileName must be supplied in the request body. The Document must be passed as octet-stream in base64 embedded format.
549 |
550 | Endpoint: _/chat/send/document_
551 |
552 | Method: **POST**
553 |
554 |
555 | ```
556 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","FileName":"hola.txt","Document":"data:application/octet-stream;base64,aG9sYSBxdWUgdGFsCg=="}' http://localhost:8080/chat/send/document
557 | ```
558 |
559 | ---
560 |
561 | ## Send Video Message
562 |
563 | Sends a Video message. Video must be in mp4 or 3gpp and base64 encoded in embedded format. You can optionally specify a text Caption and a JpegThumbnail
564 |
565 | Endpoint: _/chat/send/video_
566 |
567 | Method: **POST**
568 |
569 |
570 | ```
571 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Caption":"Look at this", "Video":"data:image/jpeg;base64,iVBORw0KGgoAAAANSU..."}' http://localhost:8080/chat/send/video
572 | ```
573 |
574 |
575 | ---
576 |
577 | ## Send Sticker Message
578 |
579 | Sends a Sticker message. Sticker must be in image/webp format and base64 encoded in embedded format. You can optionally specify a PngThumbnail
580 |
581 | Endpoint: _/chat/send/sticker_
582 |
583 | Method: **POST**
584 |
585 |
586 | ```
587 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","PngThumbnail":"VBORgoAANSU=", "Sticker":"data:image/jpeg;base64,iVBORw0KGgoAAAANSU..."}' http://localhost:8080/chat/send/sticker
588 | ```
589 |
590 |
591 | ---
592 |
593 | ## Send Location Message
594 |
595 | Sends a Location message. Latitude and Longitude must be passed, with an optional Name
596 |
597 | Endpoint: _/chat/send/location_
598 |
599 | Method: **POST**
600 |
601 |
602 | ```
603 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Latitude":48.858370,"Longitude":2.294481,"Phone":"5491155554444","Name":"Paris"}' http://localhost:8080/chat/send/location
604 | ```
605 |
606 | ---
607 |
608 | ## Send Contact Message
609 |
610 | Sends a Contact message. Both Vcard and Name body parameters are mandatory.
611 |
612 | Endpoint: _/chat/send/contact_
613 |
614 | Method: **POST**
615 |
616 |
617 | ```
618 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Name":"Casa","Vcard":"BEGIN:VCARD\nVERSION:3.0\nN:Doe;John;;;\nFN:John Doe\nORG:Example.com Inc.;\nTITLE:Imaginary test person\nEMAIL;type=INTERNET;type=WORK;type=pref:johnDoe@example.org\nTEL;type=WORK;type=pref:+1 617 555 1212\nTEL;type=WORK:+1 (617) 555-1234\nTEL;type=CELL:+1 781 555 1212\nTEL;type=HOME:+1 202 555 1212\nitem1.ADR;type=WORK:;;2 Enterprise Avenue;Worktown;NY;01111;USA\nitem1.X-ABADR:us\nitem2.ADR;type=HOME;type=pref:;;3 Acacia Avenue;Hoitem2.X-ABADR:us\nEND:VCARD"}' http://localhost:8080/chat/send/contact
619 | ```
620 |
621 | ---
622 |
623 | ## Chat Presence Indication
624 |
625 | Sends indication if you are writing/composing a text or audio message to the other party. possible states are "composing" and "paused". if media is set to "audio" it will indicate an audio message is being recorded.
626 |
627 | endpoint: _/chat/presence_
628 |
629 | method: **POST**
630 |
631 | ```
632 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","State":"composing","Media":""}' http://localhost:8080/chat/presence
633 | ```
634 |
635 | ---
636 |
637 | ## Mark message(s) as read
638 |
639 | Indicates that one or more messages were read. Id is an array of messages Ids.
640 | Chat must always be set to the chat ID (user ID in DMs and group ID in group chats).
641 | Sender must be set in group chats and must be the user ID who sent the message.
642 |
643 | endpoint: _/chat/markread_
644 |
645 | method: **POST**
646 |
647 | ```
648 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Id":["AABBCCDD112233","IIOOPPLL43332"]","Chat":"5491155553934.0:1@s.whatsapp.net"}' http://localhost:8080/chat/markread
649 | ```
650 |
651 | ---
652 |
653 | ## React to messages
654 |
655 | Sends a reaction for an existing message. Id is the message Id to react to, if its your own message, prefix the Id with the string 'me:'
656 |
657 | endpoint: _/chat/react_
658 |
659 | method: **POST**
660 |
661 | ```
662 | curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Phone":"5491155554444","Body":"❤️","Id":"me:069EDE53E81CB5A4773587FB96CB3ED3"}' http://localhost:8080/chat/react
663 | ```
664 |
665 | ---
666 |
667 | ## Download Image
668 |
669 | Downloads an Image from a message and retrieves it Base64 media encoded. Required request parameters are: Url, MediaKey, Mimetype, FileSHA256 and FileLength
670 |
671 | endpoint: _/chat/downloadimage_
672 |
673 | method: **POST**
674 |
675 | ```
676 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Url":"https://mmg.whatsapp.net/d/f/Apah954sUug5I9GnQsmXKPUdUn3ZPKGYFnscJU02dpuD.enc","Mimetype":"image/jpeg", "FileSHA256":"nMthnfkUWQiMfNJpA6K9+ft+Dx9Mb1STs+9wMHjeo/M=","FileLength":2039,"MediaKey":"vq0RR0nYGkxm2HrpwUp3sK8A7Nr1KUcOiBHrT1hg+PU=","FileEncSHA256":"6bMVZ5dRf9JKxJSUgg4w1h3iSYA3dM8gEQxaMPwoONc="}' http://localhost:8080/chat/downloadimage
677 | ```
678 |
679 | ---
680 |
681 | ## Download Video
682 |
683 | Downloads a Video from a message and retrieves it Base64 media encoded. Required request parameters are: Url, MediaKey, Mimetype, FileSHA256 and FileLength
684 |
685 | endpoint: _/chat/downloadvideo_
686 |
687 | method: **POST**
688 |
689 | ```
690 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Url":"https://mmg.whatsapp.net/d/f/Apah954sUug5I9GnQsmXKPUdUn3ZPKGYFnscJU02dpuD.enc","Mimetype":"video/mp4", "FileSHA256":"nMthnfkUWQiMfNJpA6K9+ft+Dx9Mb1STs+9wMHjeo/M=","FileLength":2039,"MediaKey":"vq0RR0nYGkxm2HrpwUp3sK8A7Nr1KUcOiBHrT1hg+PU=","FileEncSHA256":"6bMVZ5dRf9JKxJSUgg4w1h3iSYA3dM8gEQxaMPwoONc="}' http://localhost:8080/chat/downloadvideo
691 | ```
692 |
693 | ---
694 |
695 | ## Download Audio
696 |
697 | Downloads an Audio from a message and retrieves it Base64 media encoded. Required request parameters are: Url, MediaKey, Mimetype, FileSHA256 and FileLength
698 |
699 | endpoint: _/chat/downloadaudio_
700 |
701 | method: **POST**
702 |
703 | ```
704 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Url":"https://mmg.whatsapp.net/d/f/Apah954sUug5I9GnQsmXKPUdUn3ZPKGYFnscJU02dpuD.enc","Mimetype":"audio/ogg; codecs=opus", "FileSHA256":"nMthnfkUWQiMfNJpA6K9+ft+Dx9Mb1STs+9wMHjeo/M=","FileLength":2039,"MediaKey":"vq0RR0nYGkxm2HrpwUp3sK8A7Nr1KUcOiBHrT1hg+PU=","FileEncSHA256":"6bMVZ5dRf9JKxJSUgg4w1h3iSYA3dM8gEQxaMPwoONc="}' http://localhost:8080/chat/downloadaudio
705 | ```
706 |
707 | ---
708 |
709 | ## Download Document
710 |
711 | Downloads a Document from a message and retrieves it Base64 media encoded. Required request parameters are: Url, MediaKey, Mimetype, FileSHA256 and FileLength
712 |
713 | endpoint: _/chat/downloaddocument_
714 |
715 | method: **POST**
716 |
717 | ```
718 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"Url":"https://mmg.whatsapp.net/d/f/Apah954sUug5I9GnQsmXKPUdUn3ZPKGYFnscJU02dpuD.enc","Mimetype":"application/pdf", "FileSHA256":"nMthnfkUWQiMfNJpA6K9+ft+Dx9Mb1STs+9wMHjeo/M=","FileLength":2039,"MediaKey":"vq0RR0nYGkxm2HrpwUp3sK8A7Nr1KUcOiBHrT1hg+PU=","FileEncSHA256":"6bMVZ5dRf9JKxJSUgg4w1h3iSYA3dM8gEQxaMPwoONc="}' http://localhost:8080/chat/downloaddocument
719 | ```
720 |
721 | ---
722 |
723 | ## Group
724 |
725 | The following _group_ endpoints are used to gather information or perfrom actions in chat groups.
726 |
727 | ## List subscribed groups
728 |
729 | Returns complete list of subscribed groups
730 |
731 | endpoint: _/group/list_
732 |
733 | method: **GET**
734 |
735 |
736 | ```
737 | curl -s -X GET -H 'Token: 1234ABCD' http://localhost:8080/group/list
738 | ````
739 |
740 | Response:
741 | ```json
742 | {
743 | "code": 200,
744 | "data": {
745 | "Groups": [
746 | {
747 | "AnnounceVersionID": "1650572126123738",
748 | "DisappearingTimer": 0,
749 | "GroupCreated": "2022-04-21T17:15:26-03:00",
750 | "IsAnnounce": false,
751 | "IsEphemeral": false,
752 | "IsLocked": false,
753 | "JID": "120362023605733675@g.us",
754 | "Name": "Super Group",
755 | "NameSetAt": "2022-04-21T17:15:26-03:00",
756 | "NameSetBy": "5491155554444@s.whatsapp.net",
757 | "OwnerJID": "5491155554444@s.whatsapp.net",
758 | "ParticipantVersionID": "1650234126145738",
759 | "Participants": [
760 | {
761 | "IsAdmin": true,
762 | "IsSuperAdmin": true,
763 | "JID": "5491155554444@s.whatsapp.net"
764 | },
765 | {
766 | "IsAdmin": false,
767 | "IsSuperAdmin": false,
768 | "JID": "5491155553333@s.whatsapp.net"
769 | },
770 | {
771 | "IsAdmin": false,
772 | "IsSuperAdmin": false,
773 | "JID": "5491155552222@s.whatsapp.net"
774 | }
775 | ],
776 | "Topic": "",
777 | "TopicID": "",
778 | "TopicSetAt": "0001-01-01T00:00:00Z",
779 | "TopicSetBy": ""
780 | }
781 | ]
782 | },
783 | "success": true
784 | }
785 | ```
786 |
787 | ---
788 |
789 | ## Get group invite link
790 |
791 | Gets the invite link for a group
792 |
793 | endpoint: _/group/invitelink_
794 |
795 | method: **GET**
796 |
797 |
798 | ```
799 | curl -s -X GET -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"GroupJID":"120362023605733675@g.us"}' http://localhost:8080/group/invitelink
800 | ```
801 |
802 | Response:
803 |
804 | ```json
805 | {
806 | "code": 200,
807 | "data": {
808 | "InviteLink": "https://chat.whatsapp.com/HffXhYmzzyJGec61oqMXiz"
809 | },
810 | "success": true
811 | }
812 | ```
813 |
814 | ---
815 |
816 | ## Gets group information
817 |
818 | Retrieves information about a specific group
819 |
820 | endpoint: _/group/info_
821 |
822 | method: **GET**
823 |
824 |
825 | ```
826 | curl -s -X GET -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"GroupJID":"120362023605733675@g.us"}' http://localhost:8080/group/info
827 | ```
828 |
829 | Response:
830 |
831 | ```json
832 | {
833 | "code": 200,
834 | "data": {
835 | "AnnounceVersionID": "1650572126123738",
836 | "DisappearingTimer": 0,
837 | "GroupCreated": "2022-04-21T17:15:26-03:00",
838 | "IsAnnounce": false,
839 | "IsEphemeral": false,
840 | "IsLocked": false,
841 | "JID": "120362023605733675@g.us",
842 | "Name": "Super Group",
843 | "NameSetAt": "2022-04-21T17:15:26-03:00",
844 | "NameSetBy": "5491155554444@s.whatsapp.net",
845 | "OwnerJID": "5491155554444@s.whatsapp.net",
846 | "ParticipantVersionID": "1650234126145738",
847 | "Participants": [
848 | {
849 | "IsAdmin": true,
850 | "IsSuperAdmin": true,
851 | "JID": "5491155554444@s.whatsapp.net"
852 | },
853 | {
854 | "IsAdmin": false,
855 | "IsSuperAdmin": false,
856 | "JID": "5491155553333@s.whatsapp.net"
857 | },
858 | {
859 | "IsAdmin": false,
860 | "IsSuperAdmin": false,
861 | "JID": "5491155552222@s.whatsapp.net"
862 | }
863 | ],
864 | "Topic": "",
865 | "TopicID": "",
866 | "TopicSetAt": "0001-01-01T00:00:00Z",
867 | "TopicSetBy": ""
868 | },
869 | "success": true
870 | }
871 | ```
872 |
873 | ---
874 |
875 | ## Changes group photo
876 |
877 | Allows you to change a group photo/image
878 |
879 | endpoint: _/group/photo_
880 |
881 | method: **POST**
882 |
883 |
884 | ```
885 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' -d '{"GroupJID":"120362023605733675@g.us","Image":"data:image/jpeg;base64,AABB00DD-"}' http://localhost:8080/group/photo
886 | ```
887 |
888 | Response:
889 |
890 | ```json
891 | {
892 | "code": 200,
893 | "data": {
894 | "Details": "Group Photo set successfully",
895 | "PictureID": "122233212312"
896 | },
897 | "success": true
898 | }
899 | ```
900 |
901 |
902 | ---
903 |
904 | ## Changes group name
905 |
906 | Allows you to change a group name
907 |
908 | endpoint: _/group/name_
909 |
910 | method: **POST**
911 |
912 |
913 |
914 | ```
915 | curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' -d '{"GroupJID":"120362023605733675@g.us","Name":"New Group Name"}' http://localhost:8080/group/name
916 | ```
917 |
918 | Response:
919 |
920 | ```json
921 | {
922 | "code": 200,
923 | "data": {
924 | "Details": "Group Name set successfully"
925 | },
926 | "success": true
927 | }
928 | ```
929 |
930 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.23-alpine3.20 AS builder
2 |
3 | RUN apk update && apk add --no-cache gcc musl-dev gcompat
4 |
5 | WORKDIR /app
6 | COPY go.mod go.sum ./
7 | RUN go mod download
8 |
9 | COPY . .
10 | ENV CGO_ENABLED=1
11 | RUN go build -o wuzapi
12 |
13 | FROM alpine:3.20
14 |
15 | RUN apk update && apk add --no-cache \
16 | ca-certificates \
17 | netcat-openbsd \
18 | postgresql-client \
19 | openssl \
20 | curl \
21 | ffmpeg \
22 | tzdata
23 |
24 | ENV TZ="America/Sao_Paulo"
25 | WORKDIR /app
26 |
27 | COPY --from=builder /app/wuzapi /app/
28 | COPY --from=builder /app/static /app/static/
29 | COPY --from=builder /app/migrations /app/migrations/
30 | COPY --from=builder /app/files /app/files/
31 | COPY --from=builder /app/repository /app/repository/
32 | COPY --from=builder /app/dbdata /app/dbdata/
33 | COPY --from=builder /app/wuzapi.service /app/wuzapi.service
34 |
35 | RUN chmod +x /app/wuzapi
36 | RUN chmod -R 755 /app
37 | RUN chown -R root:root /app
38 |
39 | VOLUME [ "/app/dbdata", "/app/files" ]
40 |
41 | ENTRYPOINT ["/app/wuzapi", "--logtype=console", "--color=true"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Nicolas
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WuzAPI
2 |
3 | WuzAPI is an implementation
4 | of [@tulir/whatsmeow](https://github.com/tulir/whatsmeow) library as a
5 | simple RESTful API service with multiple device support and concurrent
6 | sessions.
7 |
8 | Whatsmeow does not use Puppeteer on headless Chrome, nor an Android emulator.
9 | It talks directly to WhatsApp websocket servers, thus is quite fast and uses
10 | much less memory and CPU than those solutions. The drawback is that a change
11 | in the WhatsApp protocol could break connections and will require a library
12 | update.
13 |
14 | ## :warning: Warning
15 |
16 | **Using this software violating WhatsApp ToS can get your number banned**:
17 | Be very careful, do not use this to send SPAM or anything like it. Use at
18 | your own risk. If you need to develop something with commercial interest
19 | you should contact a WhatsApp global solution provider and sign up for the
20 | Whatsapp Business API service instead.
21 |
22 | ## Available endpoints
23 |
24 | * Session: connect, disconnect and logout from WhatsApp. Retrieve
25 | connection status. Retrieve QR code for scanning.
26 | * Messages: send text, image, audio, document, template, video, sticker,
27 | location and contact messages.
28 | * Users: check if phones have whatsapp, get user information, get user avatar,
29 | retrieve full contact list.
30 | * Chat: set presence (typing/paused,recording media), mark messages as read,
31 | download images from messages, send reactions.
32 | * Groups: list subscribed, get info, get invite links, change photo and name.
33 | * Webhooks: set and get webhook that will be called whenever events/messages
34 | are received.
35 |
36 | ## Prerequisites
37 |
38 | Packages:
39 |
40 | * Go (Go Programming Language)
41 |
42 | Optional:
43 |
44 | * Docker (Containerization)
45 |
46 | ## Atualização de dependências
47 |
48 | Este projeto utiliza a biblioteca whatsmeow para comunicação com o WhatsApp. Para atualizar a biblioteca para a versão mais recente, execute:
49 |
50 | ```bash
51 | go get -u go.mau.fi/whatsmeow@latest
52 | go mod tidy
53 | ```
54 |
55 | ## Building
56 |
57 | ```
58 | go build .
59 | ```
60 |
61 | ## Run
62 |
63 | By default it will start a REST service in port 8080. These are the parameters
64 | you can use to alter behaviour
65 |
66 | * -address : sets the IP address to bind the server to (default 0.0.0.0)
67 | * -port : sets the port number (default 8080)
68 | * -logtype : format for logs, either console (default) or json
69 | * -wadebug : enable whatsmeow debug, either INFO or DEBUG levels are suported
70 | * -sslcertificate : SSL Certificate File
71 | * -sslprivatekey : SSL Private Key File
72 | * -admintoken : your admin token to create, get, or delete users from database
73 | * --logtype=console --color=true
74 | * --logtype json
75 |
76 | Example:
77 |
78 | Para ter logs coloridos:
79 | ```
80 | Depois:
81 | ```
82 | ./wuzapi --logtype=console --color=true
83 | ```
84 | (ou -color no Docker, etc.)
85 |
86 | Para logs em JSON:
87 | Rode:
88 | ```
89 | ./wuzapi --logtype json Nesse caso, color é irrelevante.
90 | ```
91 |
92 | Com fuso horário:
93 | Defina TZ=America/Sao_Paulo ./wuzapi ... no seu shell, ou no Docker Compose environment: TZ=America/Sao_Paulo.
94 |
95 | ## Usage
96 |
97 | Na primeira execução, o sistema cria automaticamente um usuário "admin" com o token administrativo definido na variável de ambiente `WUZAPI_ADMIN_TOKEN` ou no parâmetro `-admintoken`. Este usuário pode ser usado para autenticação e para gerenciar outros usuários.
98 |
99 | Para interagir com a API, você deve incluir o cabeçalho `Token` nas requisições HTTP, contendo o token de autenticação do usuário. Você pode ter vários usuários (diferentes números de WhatsApp) no mesmo servidor.
100 |
101 | O daemon também serve alguns arquivos web estáticos, úteis para desenvolvimento/teste, que você pode acessar com seu navegador:
102 |
103 | * Uma referência da API Swagger em [/api](/api)
104 | * Uma página web de exemplo para conectar e escanear códigos QR em [/login](/login) (onde você precisará passar ?token=seu_token_aqui)
105 |
106 | ## ADMIN Actions
107 |
108 | Você pode listar, adicionar e excluir usuários usando os endpoints de administração. Para usar essa funcionalidade, você deve configurar o token de administrador de uma das seguintes formas:
109 |
110 | 1. Definir a variável de ambiente `WUZAPI_ADMIN_TOKEN` antes de iniciar o aplicativo
111 | ```shell
112 | export WUZAPI_ADMIN_TOKEN=seu_token_seguro
113 | ```
114 |
115 | 2. Ou passar o parâmetro `-admintoken` na linha de comando
116 | ```shell
117 | ./wuzapi -admintoken=seu_token_seguro
118 | ```
119 |
120 | Então você pode usar o endpoint `/admin/users` com o cabeçalho `Authorization` contendo o token para:
121 | - `GET /admin/users` - Listar todos os usuários
122 | - `POST /admin/users` - Criar um novo usuário
123 | - `DELETE /admin/users/{id}` - Remover um usuário
124 |
125 | O corpo JSON para criar um novo usuário deve conter:
126 |
127 | - `name` [string] : Nome do usuário
128 | - `token` [string] : Token de segurança para autorizar/autenticar este usuário
129 | - `webhook` [string] : URL para enviar eventos via POST (opcional)
130 | - `events` [string] : Lista de eventos separados por vírgula a serem recebidos (opcional) - Eventos válidos são: "Message", "ReadReceipt", "Presence", "HistorySync", "ChatPresence", "All"
131 | - `expiration` [int] : Timestamp de expiração (opcional, não é aplicado pelo sistema)
132 |
133 | ## API reference
134 |
135 | API calls should be made with content type json, and parameters sent into the
136 | request body, always passing the Token header for authenticating the request.
137 |
138 | Check the [API Reference](https://github.com/asternic/wuzapi/blob/main/API.md)
139 |
140 | ## License
141 |
142 | Copyright © 2022 Nicolás Gudiño
143 |
144 | [MIT](https://choosealicense.com/licenses/mit/)
145 |
146 | Permission is hereby granted, free of charge, to any person obtaining a copy of
147 | this software and associated documentation files (the "Software"), to deal in
148 | the Software without restriction, including without limitation the rights to
149 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
150 | of the Software, and to permit persons to whom the Software is furnished to do
151 | so, subject to the following conditions:
152 |
153 | The above copyright notice and this permission notice shall be included in all
154 | copies or substantial portions of the Software.
155 |
156 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
157 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
158 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
159 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
160 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
161 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
162 | SOFTWARE.
163 |
164 | ## Icon Attribution
165 |
166 | [Communication icons created by Vectors Market -
167 | Flaticon](https://www.flaticon.com/free-icons/communication)
168 |
169 | ## Legal
170 |
171 | This code is in no way affiliated with, authorized, maintained, sponsored or
172 | endorsed by WhatsApp or any of its affiliates or subsidiaries. This is an
173 | independent and unofficial software. Use at your own risk.
174 |
175 | ## Cryptography Notice
176 |
177 | This distribution includes cryptographic software. The country in which you
178 | currently reside may have restrictions on the import, possession, use, and/or
179 | re-export to another country, of encryption software. BEFORE using any
180 | encryption software, please check your country's laws, regulations and policies
181 | concerning the import, possession, or use, and re-export of encryption
182 | software, to see if this is permitted. See
183 | [http://www.wassenaar.org/](http://www.wassenaar.org/) for more information.
184 |
185 | The U.S. Government Department of Commerce, Bureau of Industry and Security
186 | (BIS), has classified this software as Export Commodity Control Number (ECCN)
187 | 5D002.C.1, which includes information security software using or performing
188 | cryptographic functions with asymmetric algorithms. The form and manner of this
189 | distribution makes it eligible for export under the License Exception ENC
190 | Technology Software Unrestricted (TSU) exception (see the BIS Export
191 | Administration Regulations, Section 740.13) for both object code and source
192 | code.
193 |
194 |
195 |
--------------------------------------------------------------------------------
/docker-compose-swarm.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | wuzapi-server:
5 | image: setupautomatizado/wuzapi-server:latest
6 | networks:
7 | - network_public
8 | environment:
9 | - WUZAPI_ADMIN_TOKEN=H4Zbhw72PBKdTIgS
10 | - DB_USER=wuzapi
11 | - DB_PASSWORD=wuzapi
12 | - DB_NAME=wuzapi
13 | - DB_HOST=db
14 | - DB_PORT=5432
15 | - TZ=America/Sao_Paulo
16 | volumes:
17 | - wuzapi_dbdata:/app/dbdata
18 | - wuzapi_files:/app/files
19 | deploy:
20 | mode: replicated
21 | replicas: 1
22 | restart_policy:
23 | condition: on-failure
24 | placement:
25 | constraints: [node.role == manager]
26 | resources:
27 | limits:
28 | cpus: "1"
29 | memory: 512MB
30 | labels:
31 | - traefik.enable=true
32 | - traefik.http.routers.wuzapi-server.rule=Host(`api.wuzapi.app`)
33 | - traefik.http.routers.wuzapi-server.entrypoints=websecure
34 | - traefik.http.routers.wuzapi-server.priority=1
35 | - traefik.http.routers.wuzapi-server.tls.certresolver=letsencryptresolver
36 | - traefik.http.routers.wuzapi-server.service=wuzapi-server
37 | - traefik.http.services.wuzapi-server.loadbalancer.server.port=8080
38 | # healthcheck:
39 | # test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
40 | # interval: 10s
41 | # timeout: 5s
42 | # retries: 3
43 | # start_period: 10s
44 |
45 | networks:
46 | network_public:
47 | name: network_public
48 | external: true
49 |
50 | volumes:
51 | wuzapi_dbdata:
52 | external: true
53 | name: wuzapi_dbdata
54 | wuzapi_files:
55 | external: true
56 | name: wuzapi_files
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | wuzapi-server:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | ports:
7 | - "8080:8080"
8 | environment:
9 | - WUZAPI_ADMIN_TOKEN=H4Zbhw72PBKdTIgS
10 | - DB_USER=wuzapi
11 | - DB_PASSWORD=wuzapi
12 | - DB_NAME=wuzapi
13 | - DB_HOST=db
14 | - DB_PORT=5432
15 | - TZ=America/Sao_Paulo
16 | volumes:
17 | - ./dbdata:/app/dbdata
18 | - ./files:/app/files
19 | depends_on:
20 | - db
21 | networks:
22 | - wuzapi-network
23 |
24 | db:
25 | image: postgres:15
26 | environment:
27 | POSTGRES_USER: wuzapi
28 | POSTGRES_PASSWORD: wuzapi
29 | POSTGRES_DB: wuzapi
30 | ports:
31 | - "5432:5432"
32 | volumes:
33 | - db_data:/var/lib/postgresql/data
34 | networks:
35 | - wuzapi-network
36 |
37 | networks:
38 | wuzapi-network:
39 | driver: bridge
40 |
41 | volumes:
42 | dbdata:
43 | files:
44 | db_data:
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # WuzAPI Dashboard
2 |
3 | Interface de usuário para gerenciamento do WuzAPI.
4 |
5 | ## Requisitos
6 |
7 | - Node.js 14.x ou superior
8 | - npm 6.x ou superior
9 |
10 | ## Instalação
11 |
12 | 1. Clone o repositório
13 | 2. Navegue até o diretório do frontend:
14 | ```bash
15 | cd frontend
16 | ```
17 | 3. Instale as dependências:
18 | ```bash
19 | npm install
20 | ```
21 |
22 | ## Executando o Projeto
23 |
24 | Para iniciar o servidor de desenvolvimento:
25 |
26 | ```bash
27 | npm start
28 | ```
29 |
30 | O aplicativo estará disponível em [http://localhost:3000](http://localhost:3000).
31 |
32 | ## Estrutura do Projeto
33 |
34 | ```
35 | frontend/
36 | ├── public/ # Arquivos estáticos
37 | ├── src/
38 | │ ├── components/ # Componentes React
39 | │ ├── pages/ # Páginas da aplicação
40 | │ ├── App.tsx # Componente principal
41 | │ └── index.tsx # Ponto de entrada
42 | ├── package.json # Dependências e scripts
43 | └── tsconfig.json # Configuração do TypeScript
44 | ```
45 |
46 | ## Funcionalidades
47 |
48 | - Dashboard com visão geral das instâncias
49 | - Gerenciamento de instâncias (iniciar/parar)
50 | - Configurações do sistema
51 | - Interface responsiva e moderna
52 |
53 | ## Desenvolvimento
54 |
55 | Para criar uma build de produção:
56 |
57 | ```bash
58 | npm run build
59 | ```
60 |
61 | Para testar a aplicação:
62 |
63 | ```bash
64 | npm test
65 | ```
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wuzapi-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.3",
7 | "@emotion/styled": "^11.11.0",
8 | "@mui/icons-material": "^5.15.10",
9 | "@mui/material": "^5.15.10",
10 | "@testing-library/jest-dom": "^5.17.0",
11 | "@testing-library/react": "^13.4.0",
12 | "@testing-library/user-event": "^13.5.0",
13 | "@types/jest": "^27.5.2",
14 | "@types/node": "^16.18.80",
15 | "@types/react": "^18.2.55",
16 | "@types/react-dom": "^18.2.19",
17 | "@types/swagger-ui-react": "^5.18.0",
18 | "@types/uuid": "^10.0.0",
19 | "axios": "^1.6.7",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-router-dom": "^6.22.0",
23 | "react-scripts": "5.0.1",
24 | "swagger-ui-react": "^5.20.8",
25 | "typescript": "^4.9.5",
26 | "uuid": "^11.1.0",
27 | "web-vitals": "^2.1.4"
28 | },
29 | "scripts": {
30 | "start": "set PORT=3001 && react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test",
33 | "eject": "react-scripts eject"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 | WuzAPI Dashboard
15 |
19 |
20 |
21 | Você precisa habilitar o JavaScript para executar este aplicativo.
22 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "WuzAPI",
3 | "name": "WuzAPI Dashboard",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "zpro.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
--------------------------------------------------------------------------------
/frontend/public/zpro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pedroherpeto/wuzapi/e1d689e33dcdf9b6d68fd5d6d074fe39fc4d0ab3/frontend/public/zpro.png
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Routes, Route, useNavigate, useLocation, Navigate } from 'react-router-dom';
3 | import { Box } from '@mui/material';
4 | import { AuthProvider, useAuth } from './contexts/AuthContext';
5 | import ProtectedRoute from './components/ProtectedRoute';
6 | import Navbar from './components/Navbar';
7 | import Dashboard from './pages/Dashboard';
8 | import Instances from './pages/Instances';
9 | import Login from './pages/Login';
10 | import ApiDocs from './pages/ApiDocs';
11 | import Footer from './components/Footer';
12 |
13 | const Layout = ({ children }: { children: React.ReactNode }) => (
14 |
21 |
22 |
27 | {children}
28 |
29 |
37 |
38 |
39 |
40 | );
41 |
42 | const AppContent: React.FC = () => {
43 | const { validateToken } = useAuth();
44 | const navigate = useNavigate();
45 | const location = useLocation();
46 |
47 | useEffect(() => {
48 | const checkAuth = async () => {
49 | const token = localStorage.getItem('token');
50 | if (token) {
51 | const isValid = await validateToken();
52 | if (isValid) {
53 | if (location.pathname === '/login') {
54 | navigate('/');
55 | }
56 | } else {
57 | navigate('/login');
58 | }
59 | } else {
60 | navigate('/login');
61 | }
62 | };
63 |
64 | checkAuth();
65 | }, [validateToken, navigate, location.pathname]);
66 |
67 | return (
68 |
69 | } />
70 |
74 |
75 |
76 |
77 |
78 | }
79 | />
80 |
84 |
85 |
86 |
87 |
88 | }
89 | />
90 |
94 |
95 |
96 |
97 |
98 | }
99 | />
100 | } />
101 |
102 | );
103 | };
104 |
105 | const App: React.FC = () => {
106 | return (
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default App;
--------------------------------------------------------------------------------
/frontend/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography, Link, Button, Grid } from '@mui/material';
2 | import { WhatsApp as WhatsAppIcon } from '@mui/icons-material';
3 | import YouTubeIcon from '@mui/icons-material/YouTube';
4 | import InstagramIcon from '@mui/icons-material/Instagram';
5 | import EmailIcon from '@mui/icons-material/Email';
6 | import LinkIcon from '@mui/icons-material/Link';
7 |
8 | const Footer = () => {
9 | const handleWhatsAppClick = () => {
10 | window.open('https://wa.me/5515998566622', '_blank');
11 | };
12 |
13 | return (
14 |
25 |
32 |
33 | {/* Coluna 1 - Logo e Copyright */}
34 |
35 |
36 |
37 |
38 | WuzApi
39 |
40 |
41 |
42 |
43 | {/* Coluna 2 - Redes Sociais */}
44 |
45 |
46 | Redes Sociais
47 |
48 |
49 |
62 |
63 | YouTube
64 |
65 |
78 |
79 | @comunidadezdg
80 |
81 |
82 |
83 |
84 | {/* Coluna 3 - Links */}
85 |
86 |
87 | Links
88 |
89 |
90 |
103 |
104 | Comunidade ZDG
105 |
106 |
119 |
120 | ZPRO
121 |
122 |
123 |
124 |
125 | {/* Coluna 4 - Contato */}
126 |
127 |
128 | Contato
129 |
130 |
131 |
142 |
143 | comunidadezdg@gmail.com
144 |
145 | }
148 | sx={{
149 | color: '#8696a0',
150 | textTransform: 'none',
151 | justifyContent: 'flex-start',
152 | padding: 0,
153 | '&:hover': {
154 | color: '#00a884',
155 | background: 'transparent'
156 | }
157 | }}
158 | >
159 | +55 15 99856-6622
160 |
161 |
162 |
163 |
164 |
165 |
166 | );
167 | };
168 |
169 | export default Footer;
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AppBar, Toolbar, Typography, Button, Box, Container } from '@mui/material';
3 | import { Link as RouterLink, useNavigate, useLocation } from 'react-router-dom';
4 | import { useAuth } from '../contexts/AuthContext';
5 | import DashboardIcon from '@mui/icons-material/Dashboard';
6 | import StorageIcon from '@mui/icons-material/Storage';
7 | import LogoutIcon from '@mui/icons-material/Logout';
8 | import DescriptionIcon from '@mui/icons-material/Description';
9 | import WhatsAppIcon from '@mui/icons-material/WhatsApp';
10 |
11 | const Navbar: React.FC = () => {
12 | const { isAuthenticated, logout } = useAuth();
13 | const navigate = useNavigate();
14 | const location = useLocation();
15 |
16 | const handleLogout = () => {
17 | logout();
18 | navigate('/login');
19 | };
20 |
21 | if (!isAuthenticated) {
22 | return null;
23 | }
24 |
25 | const isActive = (path: string) => location.pathname === path;
26 |
27 | return (
28 |
37 |
46 |
47 |
48 |
49 |
60 | WuzAPI
61 |
62 |
63 |
64 |
65 | }
69 | sx={{
70 | color: isActive('/') ? '#00a884' : '#8696a0',
71 | '&:hover': {
72 | color: '#00a884',
73 | },
74 | minWidth: '120px',
75 | }}
76 | >
77 | Dashboard
78 |
79 | }
83 | sx={{
84 | color: isActive('/instances') ? '#00a884' : '#8696a0',
85 | '&:hover': {
86 | color: '#00a884',
87 | },
88 | minWidth: '120px',
89 | }}
90 | >
91 | Instâncias
92 |
93 | }
97 | sx={{
98 | color: isActive('/docs') ? '#00a884' : '#8696a0',
99 | '&:hover': {
100 | color: '#00a884',
101 | },
102 | minWidth: '120px',
103 | }}
104 | >
105 | API Docs
106 |
107 |
108 |
109 |
110 |
111 | }
114 | sx={{
115 | color: '#8696a0',
116 | '&:hover': {
117 | color: '#ea4335',
118 | },
119 | }}
120 | >
121 | Sair
122 |
123 |
124 |
125 |
126 | );
127 | };
128 |
129 | export default Navbar;
--------------------------------------------------------------------------------
/frontend/src/components/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Navigate } from 'react-router-dom';
3 | import { useAuth } from '../contexts/AuthContext';
4 |
5 | interface ProtectedRouteProps {
6 | children: React.ReactNode;
7 | }
8 |
9 | const ProtectedRoute: React.FC = ({ children }) => {
10 | const { isAuthenticated } = useAuth();
11 |
12 | if (!isAuthenticated) {
13 | return ;
14 | }
15 |
16 | return <>{children}>;
17 | };
18 |
19 | export default ProtectedRoute;
--------------------------------------------------------------------------------
/frontend/src/contexts/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | // Configuração da URL base do axios
5 | const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5555';
6 | console.log('API URL:', API_URL);
7 | axios.defaults.baseURL = API_URL;
8 |
9 | interface AuthContextType {
10 | isAuthenticated: boolean;
11 | validateToken: () => Promise;
12 | login: (token: string) => Promise;
13 | logout: () => void;
14 | }
15 |
16 | const AuthContext = createContext(undefined);
17 |
18 | export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
19 | const [isAuthenticated, setIsAuthenticated] = useState(false);
20 |
21 | const validateToken = async (): Promise => {
22 | try {
23 | const token = localStorage.getItem('token');
24 | if (!token) {
25 | console.log('Token não encontrado no localStorage');
26 | setIsAuthenticated(false);
27 | return false;
28 | }
29 |
30 | console.log('Validando token...');
31 | const response = await axios.get('/api/validate-token', {
32 | headers: {
33 | 'Authorization': `Bearer ${token}`
34 | }
35 | });
36 |
37 | console.log('Resposta da validação:', response.data);
38 | if (response.data.valid) {
39 | setIsAuthenticated(true);
40 | return true;
41 | } else {
42 | setIsAuthenticated(false);
43 | return false;
44 | }
45 | } catch (error) {
46 | console.error('Erro ao validar token:', error);
47 | if (axios.isAxiosError(error)) {
48 | console.error('Detalhes do erro:', {
49 | status: error.response?.status,
50 | data: error.response?.data,
51 | url: error.config?.url
52 | });
53 | }
54 | setIsAuthenticated(false);
55 | return false;
56 | }
57 | };
58 |
59 | const login = async (token: string) => {
60 | try {
61 | console.log('Iniciando login...');
62 | localStorage.setItem('token', token);
63 | axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
64 | const isValid = await validateToken();
65 | if (!isValid) {
66 | throw new Error('Token inválido');
67 | }
68 | console.log('Login realizado com sucesso');
69 | } catch (error) {
70 | console.error('Erro no login:', error);
71 | localStorage.removeItem('token');
72 | delete axios.defaults.headers.common['Authorization'];
73 | throw error;
74 | }
75 | };
76 |
77 | const logout = () => {
78 | console.log('Realizando logout...');
79 | localStorage.removeItem('token');
80 | delete axios.defaults.headers.common['Authorization'];
81 | setIsAuthenticated(false);
82 | };
83 |
84 | useEffect(() => {
85 | console.log('Verificando autenticação inicial...');
86 | validateToken();
87 | }, []);
88 |
89 | return (
90 |
91 | {children}
92 |
93 | );
94 | };
95 |
96 | export const useAuth = () => {
97 | const context = useContext(AuthContext);
98 | if (context === undefined) {
99 | throw new Error('useAuth deve ser usado dentro de um AuthProvider');
100 | }
101 | return context;
102 | };
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import { ThemeProvider, createTheme } from '@mui/material';
5 | import CssBaseline from '@mui/material/CssBaseline';
6 | import App from './App';
7 |
8 | const theme = createTheme({
9 | palette: {
10 | mode: 'dark',
11 | primary: {
12 | main: '#00a884',
13 | light: '#00bd94',
14 | dark: '#008f6f',
15 | },
16 | secondary: {
17 | main: '#8696a0',
18 | light: '#a4b0b7',
19 | dark: '#697780',
20 | },
21 | background: {
22 | default: '#111b21',
23 | paper: '#202c33',
24 | },
25 | text: {
26 | primary: '#e9edef',
27 | secondary: '#8696a0',
28 | },
29 | divider: '#374045',
30 | },
31 | typography: {
32 | fontFamily: '"Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif',
33 | h4: {
34 | fontWeight: 400,
35 | color: '#e9edef',
36 | },
37 | h6: {
38 | fontWeight: 400,
39 | color: '#e9edef',
40 | },
41 | body1: {
42 | color: '#e9edef',
43 | },
44 | body2: {
45 | color: '#8696a0',
46 | },
47 | },
48 | components: {
49 | MuiButton: {
50 | styleOverrides: {
51 | root: {
52 | textTransform: 'none',
53 | borderRadius: 8,
54 | },
55 | contained: {
56 | boxShadow: 'none',
57 | '&:hover': {
58 | boxShadow: 'none',
59 | },
60 | },
61 | },
62 | },
63 | MuiPaper: {
64 | styleOverrides: {
65 | root: {
66 | backgroundImage: 'none',
67 | borderRadius: 12,
68 | },
69 | },
70 | },
71 | MuiTextField: {
72 | styleOverrides: {
73 | root: {
74 | '& .MuiOutlinedInput-root': {
75 | borderRadius: 8,
76 | '& fieldset': {
77 | borderColor: '#374045',
78 | },
79 | '&:hover fieldset': {
80 | borderColor: '#00a884',
81 | },
82 | '&.Mui-focused fieldset': {
83 | borderColor: '#00a884',
84 | },
85 | },
86 | },
87 | },
88 | },
89 | MuiTableCell: {
90 | styleOverrides: {
91 | root: {
92 | borderColor: '#374045',
93 | },
94 | head: {
95 | fontWeight: 600,
96 | },
97 | },
98 | },
99 | },
100 | });
101 |
102 | const root = ReactDOM.createRoot(
103 | document.getElementById('root') as HTMLElement
104 | );
105 |
106 | root.render(
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
--------------------------------------------------------------------------------
/frontend/src/pages/ApiDocs.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Box,
4 | // Typography
5 | } from '@mui/material';
6 |
7 | const ApiDocs: React.FC = () => {
8 | return (
9 |
10 | {/*
11 |
12 | Documentação da API
13 |
14 | */}
15 |
23 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default ApiDocs;
--------------------------------------------------------------------------------
/frontend/src/pages/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Grid, Paper, Typography, Box, CircularProgress } from '@mui/material';
3 | import axios from 'axios';
4 |
5 | interface Instance {
6 | id: number;
7 | name: string;
8 | token: string;
9 | connected: boolean;
10 | loggedIn: boolean;
11 | qrcode?: string;
12 | }
13 |
14 | const Dashboard: React.FC = () => {
15 | const [instances, setInstances] = useState([]);
16 | const [loading, setLoading] = useState(true);
17 |
18 | useEffect(() => {
19 | const fetchInstances = async () => {
20 | try {
21 | const token = localStorage.getItem('token');
22 | const response = await axios.get(`${process.env.REACT_APP_API_URL}/admin/users`, {
23 | headers: { 'Authorization': `Bearer ${token}` }
24 | });
25 |
26 | // Para cada instância, verifica o status
27 | const instancesWithStatus = await Promise.all(
28 | response.data.instances.map(async (instance: Instance) => {
29 | try {
30 | const statusResponse = await axios.get(`${process.env.REACT_APP_API_URL}/session/status`, {
31 | headers: {
32 | 'Authorization': `Bearer ${token}`,
33 | 'token': instance.token
34 | }
35 | });
36 |
37 | return {
38 | ...instance,
39 | connected: statusResponse.data.data.Connected,
40 | loggedIn: statusResponse.data.data.LoggedIn
41 | };
42 | } catch (error) {
43 | return {
44 | ...instance,
45 | connected: false,
46 | loggedIn: false
47 | };
48 | }
49 | })
50 | );
51 |
52 | setInstances(instancesWithStatus);
53 | } catch (error) {
54 | console.error('Erro ao buscar instâncias:', error);
55 | } finally {
56 | setLoading(false);
57 | }
58 | };
59 |
60 | fetchInstances();
61 | }, []);
62 |
63 | if (loading) {
64 | return (
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | const totalInstances = instances.length;
72 | const activeInstances = instances.filter(instance => instance.connected).length;
73 | const loggedInstances = instances.filter(instance => instance.loggedIn).length;
74 |
75 | return (
76 |
77 |
78 | Dashboard
79 |
80 |
81 |
82 |
90 |
91 | Total de Instâncias
92 |
93 |
94 | {totalInstances}
95 |
96 |
97 |
98 |
99 |
107 |
108 | Instâncias Ativas
109 |
110 |
111 | {activeInstances}
112 |
113 |
114 |
115 |
116 |
124 |
125 | Instâncias Logadas
126 |
127 |
128 | {loggedInstances}
129 |
130 |
131 |
132 |
133 |
134 | );
135 | };
136 |
137 | export default Dashboard;
--------------------------------------------------------------------------------
/frontend/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import {
4 | Box,
5 | Paper,
6 | Typography,
7 | TextField,
8 | Button,
9 | Alert,
10 | Container,
11 | } from '@mui/material';
12 | import WhatsAppIcon from '@mui/icons-material/WhatsApp';
13 | import { useAuth } from '../contexts/AuthContext';
14 |
15 | const Login: React.FC = () => {
16 | const [token, setToken] = useState('');
17 | const [error, setError] = useState('');
18 | const { login } = useAuth();
19 | const navigate = useNavigate();
20 |
21 | const handleSubmit = async (e: React.FormEvent) => {
22 | e.preventDefault();
23 | setError('');
24 |
25 | try {
26 | await login(token);
27 | navigate('/');
28 | } catch (err) {
29 | setError('Token inválido. Por favor, verifique e tente novamente.');
30 | }
31 | };
32 |
33 | return (
34 |
35 |
43 |
57 |
69 |
70 |
71 |
72 |
81 | WuzAPI Manager
82 |
83 |
84 |
92 | Faça login para acessar o dashboard
93 |
94 |
95 | {error && (
96 |
108 | {error}
109 |
110 | )}
111 |
112 |
122 |
123 |
131 | Token de Acesso
132 |
133 |
134 | setToken(e.target.value)}
141 | autoFocus
142 | sx={{
143 | '& .MuiOutlinedInput-root': {
144 | bgcolor: 'action.hover',
145 | },
146 | }}
147 | />
148 |
149 |
157 | Token usado para autenticar suas requisições à API.
158 |
159 |
160 |
161 |
176 | Entrar
177 |
178 |
179 |
180 |
181 |
182 | );
183 | };
184 |
185 | export default Login;
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"]
20 | }
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module wuzapi
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.23.1
6 |
7 | require (
8 | github.com/go-resty/resty/v2 v2.11.0
9 | github.com/gorilla/mux v1.8.0
10 | github.com/justinas/alice v1.2.0
11 | github.com/mdp/qrterminal/v3 v3.0.0
12 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
13 | github.com/patrickmn/go-cache v2.1.0+incompatible
14 | github.com/rs/zerolog v1.33.0
15 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
16 | github.com/vincent-petithory/dataurl v1.0.0
17 | go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82
18 | google.golang.org/protobuf v1.36.5
19 | )
20 |
21 | require github.com/lib/pq v1.10.9
22 |
23 | require (
24 | github.com/go-audio/audio v1.0.0 // indirect
25 | github.com/go-audio/riff v1.0.0 // indirect
26 | github.com/go-audio/wav v1.1.0 // indirect
27 | github.com/golang-migrate/migrate/v4 v4.18.1 // indirect
28 | github.com/google/go-cmp v0.6.0 // indirect
29 | github.com/rs/cors v1.11.1 // indirect
30 | golang.org/x/time v0.5.0 // indirect
31 | )
32 |
33 | require (
34 | filippo.io/edwards25519 v1.1.0 // indirect
35 | github.com/google/uuid v1.6.0 // indirect
36 | github.com/gorilla/websocket v1.5.0 // indirect
37 | github.com/jmoiron/sqlx v1.4.0
38 | github.com/joho/godotenv v1.5.1
39 | github.com/mattn/go-colorable v0.1.13 // indirect
40 | github.com/mattn/go-isatty v0.0.19 // indirect
41 | github.com/rs/xid v1.6.0 // indirect
42 | go.mau.fi/libsignal v0.1.2 // indirect
43 | go.mau.fi/util v0.8.6 // indirect
44 | golang.org/x/crypto v0.36.0 // indirect
45 | golang.org/x/net v0.37.0 // indirect
46 | golang.org/x/sys v0.31.0 // indirect
47 | rsc.io/qr v0.2.0 // indirect
48 | )
49 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
7 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
8 | github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
9 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
10 | github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
11 | github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
12 | github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
13 | github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
14 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
15 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
16 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
17 | github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
18 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
19 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
20 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
21 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
22 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
23 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
24 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
25 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
26 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
27 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
28 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
29 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
30 | github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
31 | github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
32 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
33 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
34 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
35 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
36 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
37 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
38 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
39 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
40 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
41 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
42 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
43 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
44 | github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
45 | github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ=
46 | github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0=
47 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
48 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
49 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
50 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
54 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
55 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
56 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
57 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
58 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
59 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
60 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
61 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
62 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
63 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
64 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
65 | github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
66 | github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
67 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
68 | go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
69 | go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
70 | go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
71 | go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
72 | go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvPH7hYznmwc3gPaYqGZ5w8A=
73 | go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
75 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
76 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
77 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
78 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
79 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
80 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
81 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
82 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
83 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
84 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
85 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
86 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
87 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
88 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
89 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
90 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
91 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
92 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
93 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
94 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
95 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
103 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
104 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
105 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
106 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
107 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
108 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
109 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
110 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
111 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
112 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
113 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
114 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
115 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
116 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
117 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
118 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
119 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
120 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
121 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
122 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
123 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
124 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
125 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
126 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
127 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
128 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
129 | rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
130 | rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
131 |
--------------------------------------------------------------------------------
/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 | "regexp"
11 | "strconv"
12 |
13 | "github.com/rs/zerolog/log"
14 | )
15 |
16 | func Find(slice []string, val string) bool {
17 | for _, item := range slice {
18 | if item == val {
19 | return true
20 | }
21 | }
22 | return false
23 | }
24 |
25 | // Update entry in User map
26 | func updateUserInfo(values interface{}, field string, value string) interface{} {
27 | log.Debug().Str("field", field).Str("value", value).Msg("User info updated")
28 | values.(Values).m[field] = value
29 | return values
30 | }
31 |
32 | // webhook for regular messages
33 | func callHook(myurl string, payload map[string]string, id int) {
34 | log.Info().Str("url", myurl).Msg("Sending POST to client " + strconv.Itoa(id))
35 |
36 | // Log the payload map
37 | log.Debug().Msg("Payload:")
38 | for key, value := range payload {
39 | log.Debug().Str(key, value).Msg("")
40 | }
41 |
42 | _, err := clientHttp[id].R().SetFormData(payload).Post(myurl)
43 | if err != nil {
44 | log.Debug().Str("error", err.Error())
45 | }
46 | }
47 |
48 | func callHookFile(txtid string, data map[string]string, fileName, webhookURL string) error {
49 | // Build the user directory path
50 | userDirectory := filepath.Join("./", "files", "user_"+txtid)
51 | filePath := filepath.Join(userDirectory, fileName)
52 |
53 | // Check if the file exists before sending the URL
54 | if _, err := os.Stat(filePath); os.IsNotExist(err) {
55 | return err // File does not exist, handle this error appropriately
56 | }
57 |
58 | // Generate the URL based on the user directory path
59 | baseURL := "http://localhost:5555/files/user_" + txtid + "/"
60 | fileURL := baseURL + fileName
61 |
62 | // Regular expression to detect Base64 strings (matches "data:;base64,")
63 | base64Pattern := `^data:[\w/\-]+;base64,`
64 |
65 | // Create a final payload that includes the file URL and filters out Base64 data
66 | finalPayload := make(map[string]string)
67 | for k, v := range data {
68 | // Exclude any value that matches the Base64 pattern
69 | matched, _ := regexp.MatchString(base64Pattern, v)
70 | if matched {
71 | continue
72 | }
73 | finalPayload[k] = v
74 | }
75 |
76 | // Add the file URL to the payload
77 | finalPayload["file_url"] = fileURL
78 |
79 | // Log the final payload
80 | log.Printf("Final payload to be sent: %v", finalPayload)
81 |
82 | // Convert final payload to JSON
83 | jsonPayload, err := json.Marshal(finalPayload)
84 | if err != nil {
85 | log.Error().Err(err).Msg("Error marshaling JSON")
86 | return err
87 | }
88 |
89 | // Send the webhook
90 | req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonPayload))
91 | if err != nil {
92 | log.Error().Err(err).Msg("Error creating request")
93 | return err
94 | }
95 | req.Header.Set("Content-Type", "application/json")
96 |
97 | client := &http.Client{}
98 | resp, err := client.Do(req)
99 | if err != nil {
100 | log.Error().Err(err).Msg("Error sending webhook")
101 | return err
102 | }
103 | defer resp.Body.Close()
104 |
105 | if resp.StatusCode != http.StatusOK {
106 | log.Error().Str("status", resp.Status).Msg("Webhook responded with non-200 status")
107 | return fmt.Errorf("webhook failed with status %s", resp.Status)
108 | }
109 |
110 | // Log response
111 | log.Info().Int("status", resp.StatusCode).Msg("POST request completed")
112 |
113 | return nil
114 | }
115 |
116 | // webhook for messages with file attachments
117 | // func callHookFile(myurl string, payload map[string]string, id int, file string) error {
118 | // log.Info().Str("file", file).Str("url", myurl).Msg("Sending POST")
119 |
120 | // // Criar um novo mapa para o payload final
121 | // finalPayload := make(map[string]string)
122 | // for k, v := range payload {
123 | // finalPayload[k] = v
124 | // }
125 |
126 | // // Adicionar o arquivo ao payload
127 | // finalPayload["file"] = file
128 |
129 | // log.Debug().Interface("finalPayload", finalPayload).Msg("Final payload to be sent")
130 |
131 | // resp, err := clientHttp[id].R().
132 | // SetFiles(map[string]string{
133 | // "file": file,
134 | // }).
135 | // SetFormData(finalPayload).
136 | // Post(myurl)
137 |
138 | // if err != nil {
139 | // log.Error().Err(err).Str("url", myurl).Msg("Failed to send POST request")
140 | // return fmt.Errorf("failed to send POST request: %w", err)
141 | // }
142 |
143 | // // Log do payload enviado
144 | // log.Debug().Interface("payload", finalPayload).Msg("Payload sent to webhook")
145 |
146 | // // Optionally, you can log the response status and body
147 | // log.Info().Int("status", resp.StatusCode()).Str("body", string(resp.Body())).Msg("POST request completed")
148 |
149 | // return nil
150 | // }
151 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "os"
10 | "os/signal"
11 | "path/filepath"
12 | "strings"
13 | "syscall"
14 | "time"
15 |
16 | "go.mau.fi/whatsmeow/store/sqlstore"
17 | waLog "go.mau.fi/whatsmeow/util/log"
18 |
19 | "github.com/gorilla/mux"
20 | "github.com/jmoiron/sqlx"
21 | "github.com/joho/godotenv"
22 | _ "github.com/lib/pq"
23 | "github.com/patrickmn/go-cache"
24 | "github.com/rs/cors"
25 | "github.com/rs/zerolog"
26 | "github.com/rs/zerolog/log"
27 | )
28 |
29 | type server struct {
30 | db *sqlx.DB
31 | router *mux.Router
32 | exPath string
33 | }
34 |
35 | var (
36 | address = flag.String("address", "0.0.0.0", "Bind IP Address")
37 | port = flag.String("port", "8080", "Listen Port")
38 | waDebug = flag.String("wadebug", "", "Enable whatsmeow debug (INFO or DEBUG)")
39 | logType = flag.String("logtype", "console", "Type of log output (console or json)")
40 | colorOutput = flag.Bool("color", false, "Enable colored output for console logs")
41 | sslcert = flag.String("sslcertificate", "", "SSL Certificate File")
42 | sslprivkey = flag.String("sslprivatekey", "", "SSL Certificate Private Key File")
43 | adminToken = flag.String("admintoken", "", "Security Token to authorize admin actions (list/create/remove users)")
44 |
45 | container *sqlstore.Container
46 | killchannel = make(map[int](chan bool))
47 | userinfocache = cache.New(5*time.Minute, 10*time.Minute)
48 | )
49 |
50 | func init() {
51 | err := godotenv.Load()
52 | if err != nil {
53 | log.Warn().Err(err).Msg("Não foi possível carregar o arquivo .env (pode ser que não exista).")
54 | }
55 |
56 | flag.Parse()
57 |
58 | tz := os.Getenv("TZ")
59 | if tz != "" {
60 | loc, err := time.LoadLocation(tz)
61 | if err != nil {
62 | log.Warn().Err(err).Msgf("Não foi possível definir TZ=%q, usando UTC", tz)
63 | } else {
64 | time.Local = loc
65 | log.Info().Str("TZ", tz).Msg("Timezone definido pelo ambiente")
66 | }
67 | }
68 |
69 | if *logType == "json" {
70 | log.Logger = zerolog.New(os.Stdout).
71 | With().
72 | Timestamp().
73 | Str("role", filepath.Base(os.Args[0])).
74 | Logger()
75 | } else {
76 | output := zerolog.ConsoleWriter{
77 | Out: os.Stdout,
78 | TimeFormat: "2006-01-02 15:04:05 -07:00",
79 | NoColor: !*colorOutput,
80 | }
81 |
82 | output.FormatLevel = func(i interface{}) string {
83 | if i == nil {
84 | return ""
85 | }
86 | lvl := strings.ToUpper(i.(string))
87 | switch lvl {
88 | case "DEBUG":
89 | return "\x1b[34m" + lvl + "\x1b[0m"
90 | case "INFO":
91 | return "\x1b[32m" + lvl + "\x1b[0m"
92 | case "WARN":
93 | return "\x1b[33m" + lvl + "\x1b[0m"
94 | case "ERROR", "FATAL", "PANIC":
95 | return "\x1b[31m" + lvl + "\x1b[0m"
96 | default:
97 | return lvl
98 | }
99 | }
100 |
101 | log.Logger = zerolog.New(output).
102 | With().
103 | Timestamp().
104 | Str("role", filepath.Base(os.Args[0])).
105 | Logger()
106 | }
107 |
108 | if *adminToken == "" {
109 | if v := os.Getenv("WUZAPI_ADMIN_TOKEN"); v != "" {
110 | *adminToken = v
111 | }
112 | }
113 | }
114 |
115 | func main() {
116 | ex, err := os.Executable()
117 | if err != nil {
118 | panic(err)
119 | }
120 | exPath := filepath.Dir(ex)
121 |
122 | dbUser := os.Getenv("DB_USER")
123 | dbPassword := os.Getenv("DB_PASSWORD")
124 | dbName := os.Getenv("DB_NAME")
125 | dbHost := os.Getenv("DB_HOST")
126 | dbPort := os.Getenv("DB_PORT")
127 |
128 | dsn := fmt.Sprintf(
129 | "user=%s password=%s dbname=%s host=%s port=%s sslmode=disable",
130 | dbUser, dbPassword, dbName, dbHost, dbPort,
131 | )
132 |
133 | var db *sqlx.DB
134 | const maxAttempts = 10
135 | for i := 1; i <= maxAttempts; i++ {
136 | db, err = sqlx.Open("postgres", dsn)
137 | if err == nil {
138 | errPing := db.Ping()
139 | if errPing == nil {
140 | log.Info().Msgf("[DB] Conexão PostgreSQL estabelecida na tentativa %d", i)
141 | break
142 | }
143 | err = errPing
144 | }
145 | log.Warn().Msgf("[DB] Falha ao conectar (%d/%d): %v", i, maxAttempts, err)
146 | time.Sleep(2 * time.Second)
147 | }
148 | if err != nil {
149 | log.Fatal().Err(err).Msgf("[DB] Não foi possível conectar ao PostgreSQL após %d tentativas", maxAttempts)
150 | os.Exit(1)
151 | }
152 |
153 | if err := runMigrations(db, exPath); err != nil {
154 | log.Fatal().Err(err).Msg("Falha ao executar migrações")
155 | os.Exit(1)
156 | }
157 |
158 | var dbLog waLog.Logger
159 | if *waDebug != "" {
160 | dbLog = waLog.Stdout("Database", *waDebug, *colorOutput)
161 | }
162 | container, err = sqlstore.New("postgres", dsn, dbLog)
163 | if err != nil {
164 | log.Fatal().Err(err).Msg("Falha ao criar container sqlstore")
165 | os.Exit(1)
166 | }
167 |
168 | s := &server{
169 | router: mux.NewRouter(),
170 | db: db,
171 | exPath: exPath,
172 | }
173 |
174 | // Configuração do CORS
175 | corsMiddleware := cors.New(cors.Options{
176 | AllowedOrigins: []string{"*"},
177 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
178 | AllowedHeaders: []string{"Content-Type", "Authorization", "token", "instance-token"},
179 | AllowCredentials: true,
180 | })
181 |
182 | s.router.PathPrefix("/files/").Handler(http.StripPrefix("/files/", http.FileServer(http.Dir("./files"))))
183 |
184 | s.routes()
185 |
186 | s.connectOnStartup()
187 |
188 | srv := &http.Server{
189 | Addr: *address + ":" + *port,
190 | Handler: corsMiddleware.Handler(s.router), // Aplicando o middleware CORS
191 | ReadHeaderTimeout: 20 * time.Second,
192 | ReadTimeout: 60 * time.Second,
193 | WriteTimeout: 120 * time.Second,
194 | IdleTimeout: 180 * time.Second,
195 | }
196 |
197 | done := make(chan os.Signal, 1)
198 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
199 |
200 | go func() {
201 | if *sslcert != "" {
202 | if err := srv.ListenAndServeTLS(*sslcert, *sslprivkey); err != nil && err != http.ErrServerClosed {
203 | log.Fatal().Err(err).Msg("Falha ao iniciar o servidor HTTPS")
204 | }
205 | } else {
206 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
207 | log.Fatal().Err(err).Msg("Falha ao iniciar o servidor HTTP")
208 | }
209 | }
210 | }()
211 | log.Info().Str("address", *address).Str("port", *port).Msg("Servidor iniciado. Aguardando conexões...")
212 |
213 | <-done
214 | log.Warn().Msg("Servidor parando...")
215 |
216 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
217 | defer cancel()
218 | if err := srv.Shutdown(ctx); err != nil {
219 | log.Error().Err(err).Msg("Falha ao parar o servidor")
220 | os.Exit(1)
221 | }
222 | log.Info().Msg("Servidor saiu corretamente")
223 | }
224 |
225 | func runMigrations(db *sqlx.DB, exPath string) error {
226 | log.Info().Msg("Checando se já existe algum usuário...")
227 |
228 | var userCount int
229 | if err := db.Get(&userCount, "SELECT COUNT(*) FROM users;"); err == nil {
230 | if userCount > 0 {
231 | log.Info().Msgf("Usuário(s) encontrado(s): %d. Pulando migrações...", userCount)
232 | return nil
233 | }
234 | log.Warn().Msg("Nenhum usuário encontrado. Rodando migração e inserindo usuário padrão.")
235 | return applyMigrationsAndCreateUser(db, exPath)
236 | } else {
237 | log.Warn().Err(err).Msg("Erro consultando usuários (talvez a tabela nem exista). Rodando migração...")
238 | return applyMigrationsAndCreateUser(db, exPath)
239 | }
240 | }
241 |
242 | func applyMigrationsAndCreateUser(db *sqlx.DB, exPath string) error {
243 | log.Info().Msg("Executando migrações...")
244 |
245 | migFile := filepath.Join(exPath, "migrations", "0001_create_users_table.up.sql")
246 | sqlBytes, err := ioutil.ReadFile(migFile)
247 | if err != nil {
248 | return fmt.Errorf("falha ao ler arquivo de migração (%s): %w", migFile, err)
249 | }
250 | if _, err = db.Exec(string(sqlBytes)); err != nil {
251 | return fmt.Errorf("falha ao executar migração: %w", err)
252 | }
253 | log.Info().Msg("Migração executada com sucesso.")
254 |
255 | // Usar o token administrativo definido na variável de ambiente
256 | userToken := *adminToken
257 | if userToken == "" {
258 | userToken = "1234ABCD" // Valor padrão caso não esteja definido
259 | log.Warn().Msg("WUZAPI_ADMIN_TOKEN não definido, usando token padrão")
260 | }
261 |
262 | if _, err = db.Exec("INSERT INTO users (name, token) VALUES ($1, $2)", "admin", userToken); err != nil {
263 | if strings.Contains(err.Error(), "duplicate key") {
264 | log.Warn().Msg("Usuário padrão já existe. Ignorando.")
265 | return nil
266 | }
267 | return fmt.Errorf("erro ao inserir usuário padrão: %w", err)
268 | }
269 | log.Info().Msgf("Usuário padrão (admin/%s) inserido com sucesso.", userToken)
270 | return nil
271 | }
272 |
--------------------------------------------------------------------------------
/migrations/0001_create_users_table.down.sql:
--------------------------------------------------------------------------------
1 | -- migrations/0001_create_users_table.down.sql
2 | DROP TABLE users;
3 |
--------------------------------------------------------------------------------
/migrations/0001_create_users_table.up.sql:
--------------------------------------------------------------------------------
1 | -- migrations/0001_create_users_table.up.sql
2 | CREATE TABLE IF NOT EXISTS users (
3 | id SERIAL PRIMARY KEY,
4 | name TEXT NOT NULL,
5 | token TEXT NOT NULL,
6 | webhook TEXT NOT NULL DEFAULT '',
7 | jid TEXT NOT NULL DEFAULT '',
8 | qrcode TEXT NOT NULL DEFAULT '',
9 | connected INTEGER,
10 | expiration INTEGER,
11 | proxy_url TEXT,
12 | events TEXT NOT NULL DEFAULT 'All'
13 | );
--------------------------------------------------------------------------------
/repository/repository.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/jmoiron/sqlx"
7 | )
8 |
9 | // PostgresRepository é a implementação do repositório para PostgreSQL
10 | type PostgresRepository struct {
11 | db *sqlx.DB
12 | }
13 |
14 | // NewPostgresRepository cria uma nova instância do PostgresRepository
15 | func NewPostgresRepository(db *sqlx.DB) *PostgresRepository {
16 | return &PostgresRepository{db: db}
17 | }
18 |
19 | // GetAllUsers retorna todos os usuários do banco de dados
20 | // Implementação de exemplo que consulta todos os registros da tabela users
21 | func (r *PostgresRepository) GetAllUsers() ([]User, error) {
22 | var users []User
23 | query := "SELECT * FROM users"
24 | err := r.db.Select(&users, query)
25 | if err != nil {
26 | log.Printf("Erro ao buscar usuários: %v", err)
27 | return nil, err
28 | }
29 | return users, nil
30 | }
31 |
32 | // User representa a estrutura de dados de um usuário
33 | type User struct {
34 | ID int `db:"id"`
35 | Name string `db:"name"`
36 | Token string `db:"token"`
37 | Webhook string `db:"webhook"`
38 | Jid string `db:"jid"`
39 | Qrcode string `db:"qrcode"`
40 | Connected int `db:"connected"`
41 | Expiration int `db:"expiration"`
42 | Events string `db:"events"`
43 | }
44 |
--------------------------------------------------------------------------------
/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/justinas/alice"
10 | "github.com/rs/zerolog"
11 | "github.com/rs/zerolog/hlog"
12 | )
13 |
14 | type Middleware = alice.Constructor
15 |
16 | func (s *server) routes() {
17 |
18 | ex, err := os.Executable()
19 | if err != nil {
20 | panic(err)
21 | }
22 | exPath := filepath.Dir(ex)
23 |
24 | var routerLog zerolog.Logger
25 | if *logType == "json" {
26 | routerLog = zerolog.New(os.Stdout).
27 | With().
28 | Timestamp().
29 | Str("role", filepath.Base(os.Args[0])).
30 | Str("host", *address).
31 | Logger()
32 | } else {
33 | output := zerolog.ConsoleWriter{
34 | Out: os.Stdout,
35 | TimeFormat: time.RFC3339,
36 | NoColor: !*colorOutput,
37 | }
38 | routerLog = zerolog.New(output).
39 | With().
40 | Timestamp().
41 | Str("role", filepath.Base(os.Args[0])).
42 | Str("host", *address).
43 | Logger()
44 | }
45 |
46 | adminRoutes := s.router.PathPrefix("/admin").Subrouter()
47 | adminRoutes.Use(s.authadmin)
48 | adminRoutes.Handle("/users", s.ListUsers()).Methods("GET")
49 | adminRoutes.Handle("/users", s.AddUser()).Methods("POST")
50 | adminRoutes.Handle("/users/{id}", s.EditUser()).Methods("PUT")
51 | adminRoutes.Handle("/users/{id}", s.DeleteUser()).Methods("DELETE")
52 |
53 | c := alice.New()
54 | c = c.Append(s.authalice)
55 | c = c.Append(hlog.NewHandler(routerLog))
56 |
57 | c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
58 | hlog.FromRequest(r).Info().
59 | Str("method", r.Method).
60 | Stringer("url", r.URL).
61 | Int("status", status).
62 | Int("size", size).
63 | Dur("duration", duration).
64 | Str("userid", r.Context().Value("userinfo").(Values).Get("Id")).
65 | Msg("Got API Request")
66 | }))
67 | c = c.Append(hlog.RemoteAddrHandler("ip"))
68 | c = c.Append(hlog.UserAgentHandler("user_agent"))
69 | c = c.Append(hlog.RefererHandler("referer"))
70 | c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id"))
71 |
72 | s.router.Handle("/session/connect", c.Then(s.Connect())).Methods("POST")
73 | s.router.Handle("/session/disconnect", c.Then(s.Disconnect())).Methods("POST")
74 | s.router.Handle("/session/logout", c.Then(s.Logout())).Methods("POST")
75 | s.router.Handle("/session/status", c.Then(s.GetStatus())).Methods("GET")
76 | s.router.Handle("/session/qr", c.Then(s.GetQR())).Methods("GET")
77 | s.router.Handle("/session/pairphone", c.Then(s.PairPhone())).Methods("POST")
78 |
79 | s.router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST")
80 | s.router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET")
81 | s.router.Handle("/webhook", c.Then(s.DeleteWebhook())).Methods("DELETE") // Nova rota
82 | s.router.Handle("/webhook/update", c.Then(s.UpdateWebhook())).Methods("PUT") // Nova rota
83 | s.router.Handle("/session/proxy", c.Then(s.SetProxy())).Methods("POST")
84 |
85 | s.router.Handle("/chat/send/text", c.Then(s.SendMessage())).Methods("POST")
86 | s.router.Handle("/chat/send/image", c.Then(s.SendImage())).Methods("POST")
87 | s.router.Handle("/chat/send/audio", c.Then(s.SendAudio())).Methods("POST")
88 | s.router.Handle("/chat/send/document", c.Then(s.SendDocument())).Methods("POST")
89 | // s.router.Handle("/chat/send/template", c.Then(s.SendTemplate())).Methods("POST")
90 | s.router.Handle("/chat/send/video", c.Then(s.SendVideo())).Methods("POST")
91 | s.router.Handle("/chat/send/sticker", c.Then(s.SendSticker())).Methods("POST")
92 | s.router.Handle("/chat/send/location", c.Then(s.SendLocation())).Methods("POST")
93 | s.router.Handle("/chat/send/contact", c.Then(s.SendContact())).Methods("POST")
94 | s.router.Handle("/chat/react", c.Then(s.React())).Methods("POST")
95 | s.router.Handle("/user/presence", c.Then(s.SendPresence())).Methods("POST")
96 | s.router.Handle("/chat/edit", c.Then(s.Edit())).Methods("POST")
97 | s.router.Handle("/chat/revoke", c.Then(s.Revoke())).Methods("POST")
98 | s.router.Handle("/chat/send/buttons", c.Then(s.SendButtons())).Methods("POST")
99 | s.router.Handle("/chat/send/list", c.Then(s.SendList())).Methods("POST")
100 |
101 | s.router.Handle("/user/info", c.Then(s.GetUser())).Methods("POST")
102 | s.router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST")
103 | s.router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST")
104 | s.router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET")
105 |
106 | s.router.Handle("/chat/presence", c.Then(s.ChatPresence())).Methods("POST")
107 | s.router.Handle("/chat/markread", c.Then(s.MarkRead())).Methods("POST")
108 | s.router.Handle("/chat/downloadimage", c.Then(s.DownloadImage())).Methods("POST")
109 | s.router.Handle("/chat/downloadvideo", c.Then(s.DownloadVideo())).Methods("POST")
110 | s.router.Handle("/chat/downloadaudio", c.Then(s.DownloadAudio())).Methods("POST")
111 | s.router.Handle("/chat/downloaddocument", c.Then(s.DownloadDocument())).Methods("POST")
112 |
113 | s.router.Handle("/group/list", c.Then(s.ListGroups())).Methods("GET")
114 | s.router.Handle("/group/info", c.Then(s.GetGroupInfo())).Methods("GET")
115 | s.router.Handle("/group/invitelink", c.Then(s.GetGroupInviteLink())).Methods("GET")
116 | s.router.Handle("/group/photo", c.Then(s.SetGroupPhoto())).Methods("POST")
117 | s.router.Handle("/group/name", c.Then(s.SetGroupName())).Methods("POST")
118 | s.router.Handle("/newsletter/list", c.Then(s.ListNewsletter())).Methods("GET")
119 | // s.router.Handle("/newsletters/info", c.Then(s.GetNewsletterInfo())).Methods("GET")
120 |
121 | // Rota pública para validação de token
122 | s.router.HandleFunc("/api/validate-token", s.ValidateToken()).Methods("GET")
123 |
124 | // Rota para arquivos estáticos deve ser a última
125 | s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/")))
126 | }
127 |
--------------------------------------------------------------------------------
/static/api/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pedroherpeto/wuzapi/e1d689e33dcdf9b6d68fd5d6d074fe39fc4d0ab3/static/api/favicon-16x16.png
--------------------------------------------------------------------------------
/static/api/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pedroherpeto/wuzapi/e1d689e33dcdf9b6d68fd5d6d074fe39fc4d0ab3/static/api/favicon-32x32.png
--------------------------------------------------------------------------------
/static/api/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WuzAPI - Documentação
7 |
8 |
14 |
20 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
631 |
632 |
633 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pedroherpeto/wuzapi/e1d689e33dcdf9b6d68fd5d6d074fe39fc4d0ab3/static/favicon.ico
--------------------------------------------------------------------------------
/static/github-markdown-css/code-navigation-banner-illo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/static/github-markdown-css/github-css.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8"; .gist{font-size:16px;color:#333;text-align:left;/*!* GitHub Light v0.4.1 * Copyright(c) 2012 - 2017 GitHub,Inc. * Licensed under MIT(https://github.com/primer/github-syntax-theme-generator/blob/master/LICENSE) */ direction:ltr}.gist .markdown-body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;font-size:16px;line-height:1.5;word-wrap:break-word}.gist .markdown-body kbd{display:inline-block;padding:3px 5px;font:11px SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:1px solid #d1d5da;border-radius:3px;box-shadow:inset 0 -1px 0 #d1d5da}.gist .markdown-body:before{display:table;content:""}.gist .markdown-body:after{display:table;clear:both;content:""}.gist .markdown-body>:first-child{margin-top:0 !important}.gist .markdown-body>:last-child{margin-bottom:0 !important}.gist .markdown-body a:not([href]){color:inherit;text-decoration:none}.gist .markdown-body .absent{color:#cb2431}.gist .markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.gist .markdown-body .anchor:focus{outline:none}.gist .markdown-body blockquote,.gist .markdown-body details,.gist .markdown-body dl,.gist .markdown-body ol,.gist .markdown-body p,.gist .markdown-body pre,.gist .markdown-body table,.gist .markdown-body ul{margin-top:0;margin-bottom:16px}.gist .markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0}.gist .markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.gist .markdown-body blockquote>:first-child{margin-top:0}.gist .markdown-body blockquote>:last-child{margin-bottom:0}.gist .markdown-body h1,.gist .markdown-body h2,.gist .markdown-body h3,.gist .markdown-body h4,.gist .markdown-body h5,.gist .markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.gist .markdown-body h1 .octicon-link,.gist .markdown-body h2 .octicon-link,.gist .markdown-body h3 .octicon-link,.gist .markdown-body h4 .octicon-link,.gist .markdown-body h5 .octicon-link,.gist .markdown-body h6 .octicon-link{color:#1b1f23;vertical-align:middle;visibility:hidden}.gist .markdown-body h1:hover .anchor,.gist .markdown-body h2:hover .anchor,.gist .markdown-body h3:hover .anchor,.gist .markdown-body h4:hover .anchor,.gist .markdown-body h5:hover .anchor,.gist .markdown-body h6:hover .anchor{text-decoration:none}.gist .markdown-body h1:hover .anchor .octicon-link,.gist .markdown-body h2:hover .anchor .octicon-link,.gist .markdown-body h3:hover .anchor .octicon-link,.gist .markdown-body h4:hover .anchor .octicon-link,.gist .markdown-body h5:hover .anchor .octicon-link,.gist .markdown-body h6:hover .anchor .octicon-link{visibility:visible}.gist .markdown-body h1 code,.gist .markdown-body h1 tt,.gist .markdown-body h2 code,.gist .markdown-body h2 tt,.gist .markdown-body h3 code,.gist .markdown-body h3 tt,.gist .markdown-body h4 code,.gist .markdown-body h4 tt,.gist .markdown-body h5 code,.gist .markdown-body h5 tt,.gist .markdown-body h6 code,.gist .markdown-body h6 tt{font-size:inherit}.gist .markdown-body h1{font-size:2em}.gist .markdown-body h1,.gist .markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eaecef}.gist .markdown-body h2{font-size:1.5em}.gist .markdown-body h3{font-size:1.25em}.gist .markdown-body h4{font-size:1em}.gist .markdown-body h5{font-size:.875em}.gist .markdown-body h6{font-size:.85em;color:#6a737d}.gist .markdown-body ol,.gist .markdown-body ul{padding-left:2em}.gist .markdown-body ol.no-list,.gist .markdown-body ul.no-list{padding:0;list-style-type:none}.gist .markdown-body ol ol,.gist .markdown-body ol ul,.gist .markdown-body ul ol,.gist .markdown-body ul ul{margin-top:0;margin-bottom:0}.gist .markdown-body li{word-wrap:break-all}.gist .markdown-body li>p{margin-top:16px}.gist .markdown-body li+li{margin-top:.25em}.gist .markdown-body dl{padding:0}.gist .markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.gist .markdown-body dl dd{padding:0 16px;margin-bottom:16px}.gist .markdown-body table{display:block;width:100%;overflow:auto}.gist .markdown-body table th{font-weight:600}.gist .markdown-body table td,.gist .markdown-body table th{padding:6px 13px;border:1px solid #dfe2e5}.gist .markdown-body table tr{background-color:#fff;border-top:1px solid #c6cbd1}.gist .markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.gist .markdown-body table img{background-color:initial}.gist .markdown-body img{max-width:100%;box-sizing:initial;background-color:#fff}.gist .markdown-body img[align=right]{padding-left:20px}.gist .markdown-body img[align=left]{padding-right:20px}.gist .markdown-body .emoji{max-width:none;vertical-align:text-top;background-color:initial}.gist .markdown-body span.frame{display:block;overflow:hidden}.gist .markdown-body span.frame>span{display:block;float:left;width:auto;padding:7px;margin:13px 0 0;overflow:hidden;border:1px solid #dfe2e5}.gist .markdown-body span.frame span img{display:block;float:left}.gist .markdown-body span.frame span span{display:block;padding:5px 0 0;clear:both;color:#24292e}.gist .markdown-body span.align-center{display:block;overflow:hidden;clear:both}.gist .markdown-body span.align-center>span{display:block;margin:13px auto 0;overflow:hidden;text-align:center}.gist .markdown-body span.align-center span img{margin:0 auto;text-align:center}.gist .markdown-body span.align-right{display:block;overflow:hidden;clear:both}.gist .markdown-body span.align-right>span{display:block;margin:13px 0 0;overflow:hidden;text-align:right}.gist .markdown-body span.align-right span img{margin:0;text-align:right}.gist .markdown-body span.float-left{display:block;float:left;margin-right:13px;overflow:hidden}.gist .markdown-body span.float-left span{margin:13px 0 0}.gist .markdown-body span.float-right{display:block;float:right;margin-left:13px;overflow:hidden}.gist .markdown-body span.float-right>span{display:block;margin:13px auto 0;overflow:hidden;text-align:right}.gist .markdown-body code,.gist .markdown-body tt{padding:.2em .4em;margin:0;font-size:85%;background-color:#f6f8fa;border-radius:3px}.gist .markdown-body code br,.gist .markdown-body tt br{display:none}.gist .markdown-body del code{text-decoration:inherit}.gist .markdown-body pre{word-wrap:normal}.gist .markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.gist .markdown-body .highlight{margin-bottom:16px}.gist .markdown-body .highlight pre{margin-bottom:0;word-break:normal}.gist .markdown-body .highlight pre,.gist .markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px}.gist .markdown-body pre code,.gist .markdown-body pre tt{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:#f6f8fa;border:0}.gist .markdown-body .csv-data td,.gist .markdown-body .csv-data th{padding:5px;overflow:hidden;font-size:12px;line-height:1;text-align:left;white-space:nowrap}.gist .markdown-body .csv-data .blob-num{padding:10px 8px 9px;text-align:right;background:#fff;border:0}.gist .markdown-body .csv-data tr{border-top:0}.gist .markdown-body .csv-data th{font-weight:600;background:#f6f8fa;border-top:0}.gist .pl-c{color:#6a737d}.gist .pl-c1,.gist .pl-s .pl-v{color:#005cc5}.gist .pl-e,.gist .pl-en{color:#6f42c1}.gist .pl-s .pl-s1,.gist .pl-smi{color:#24292e}.gist .pl-ent{color:#22863a}.gist .pl-k{color:#d73a49}.gist .pl-pds,.gist .pl-s,.gist .pl-s .pl-pse .pl-s1,.gist .pl-sr,.gist .pl-sr .pl-cce,.gist .pl-sr .pl-sra,.gist .pl-sr .pl-sre{color:#032f62}.gist .pl-smw,.gist .pl-v{color:#e36209}.gist .pl-bu{color:#b31d28}.gist .pl-ii{color:#fafbfc;background-color:#b31d28}.gist .pl-c2{color:#fafbfc;background-color:#d73a49}.gist .pl-c2:before{content:"^M"}.gist .pl-sr .pl-cce{font-weight:700;color:#22863a}.gist .pl-ml{color:#735c0f}.gist .pl-mh,.gist .pl-mh .pl-en,.gist .pl-ms{font-weight:700;color:#005cc5}.gist .pl-mi{font-style:italic;color:#24292e}.gist .pl-mb{font-weight:700;color:#24292e}.gist .pl-md{color:#b31d28;background-color:#ffeef0}.gist .pl-mi1{color:#22863a;background-color:#f0fff4}.gist .pl-mc{color:#e36209;background-color:#ffebda}.gist .pl-mi2{color:#f6f8fa;background-color:#005cc5}.gist .pl-mdr{font-weight:700;color:#6f42c1}.gist .pl-ba{color:#586069}.gist .pl-sg{color:#959da5}.gist .pl-corl{text-decoration:underline;color:#032f62}.gist .breadcrumb{font-size:16px;color:#586069}.gist .breadcrumb .separator{white-space:pre-wrap}.gist .breadcrumb .separator:after,.gist .breadcrumb .separator:before{content:" "}.gist .breadcrumb strong.final-path{color:#24292e}.gist strong{font-weight:bolder}.gist .editor-abort{display:inline;font-size:14px}.gist .blob-interaction-bar{position:relative;background-color:#f2f2f2;border-bottom:1px solid #e5e5e5}.gist .blob-interaction-bar:before{display:table;content:""}.gist .blob-interaction-bar:after{display:table;clear:both;content:""}.gist .blob-interaction-bar .octicon-search{position:absolute;top:10px;left:10px;font-size:12px;color:#586069}.gist .blob-filter{width:100%;padding:4px 20px 5px 30px;font-size:12px;border:0;border-radius:0;outline:none}.gist .blob-filter:focus{outline:none}.gist .html-blob{margin-bottom:15px}.gist .TagsearchPopover{width:inherit;max-width:600px}.gist .TagsearchPopover-content{max-height:300px}.gist .TagsearchPopover-list .TagsearchPopover-list-item:hover{background-color:#f6f8fa}.gist .TagsearchPopover-list .TagsearchPopover-list-item .TagsearchPopover-item:hover{text-decoration:none}.gist .TagsearchPopover-list .blob-code-inner{white-space:pre-wrap}.gist .linejump .linejump-input{width:340px;background-color:#fafbfc}.gist .linejump .btn,.gist .linejump .linejump-input{padding:10px 15px;font-size:16px}.gist .CopyBlock{line-height:20px;cursor:pointer}.gist .CopyBlock .octicon-clippy{display:none}.gist .CopyBlock:active,.gist .CopyBlock:focus,.gist .CopyBlock:hover{background-color:#fff;outline:none}.gist .CopyBlock:active .octicon-clippy,.gist .CopyBlock:focus .octicon-clippy,.gist .CopyBlock:hover .octicon-clippy{display:inline-block}.gist .blob-wrapper{overflow-x:auto;overflow-y:hidden}.gist .page-blob.height-full .blob-wrapper{overflow-y:auto}.gist .page-edit-blob.height-full .CodeMirror{height:300px}.gist .page-edit-blob.height-full .CodeMirror,.gist .page-edit-blob.height-full .CodeMirror-scroll{display:flex;flex-direction:column;flex:1 1 auto}.gist .blob-wrapper-embedded{max-height:240px;overflow-y:auto}.gist .diff-table{width:100%;border-collapse:initial}.gist .diff-table .line-comments{padding:10px;vertical-align:top;border-top:1px solid #e1e4e8}.gist .diff-table .line-comments:first-child+.empty-cell{border-left-width:1px}.gist .diff-table tr:not(:last-child) .line-comments{border-top:1px solid #e1e4e8;border-bottom:1px solid #e1e4e8}.gist .blob-num{width:1%;min-width:50px;padding-right:10px;padding-left:10px;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px;line-height:20px;color:rgba(27,31,35,.3);text-align:right;white-space:nowrap;vertical-align:top;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gist .blob-num:hover{color:rgba(27,31,35,.6)}.gist .blob-num:before{content:attr(data-line-number)}.gist .blob-num.non-expandable{cursor:default}.gist .blob-num.non-expandable:hover{color:rgba(27,31,35,.3)}.gist .blob-code{position:relative;padding-right:10px;padding-left:10px;line-height:20px;vertical-align:top}.gist .blob-code-inner{overflow:visible;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px;color:#24292e;word-wrap:normal;white-space:pre}.gist .blob-code-inner .x-first{border-top-left-radius:.2em;border-bottom-left-radius:.2em}.gist .blob-code-inner .x-last{border-top-right-radius:.2em;border-bottom-right-radius:.2em}.gist .blob-code-inner.highlighted,.gist .blob-code-inner .highlighted{background-color:#fffbdd}.gist .blob-code-marker:before{padding-right:8px;content:attr(data-code-marker)}.gist .blob-code-marker-addition:before{content:"+ "}.gist .blob-code-marker-deletion:before{content:"- "}.gist .blob-code-marker-context:before{content:" "}.gist .soft-wrap .diff-table{table-layout:fixed}.gist .soft-wrap .blob-code{padding-left:18px;text-indent:-7px}.gist .soft-wrap .blob-code-inner{word-wrap:break-word;white-space:pre-wrap}.gist .soft-wrap .no-nl-marker{display:none}.gist .soft-wrap .add-line-comment{margin-left:-28px}.gist .blob-code-hunk,.gist .blob-num-expandable,.gist .blob-num-hunk{color:rgba(27,31,35,.7);vertical-align:middle}.gist .blob-num-expandable,.gist .blob-num-hunk{background-color:#dbedff}.gist .blob-code-hunk{padding-top:4px;padding-bottom:4px;background-color:#f1f8ff;border-width:1px 0}.gist .blob-expanded .blob-code,.gist .blob-expanded .blob-num{background-color:#fafbfc}.gist .blob-expanded+tr:not(.blob-expanded) .blob-code,.gist .blob-expanded+tr:not(.blob-expanded) .blob-num,.gist .blob-expanded .blob-num-hunk,.gist tr:not(.blob-expanded)+.blob-expanded .blob-code,.gist tr:not(.blob-expanded)+.blob-expanded .blob-num{border-top:1px solid #eaecef}.gist .blob-num-expandable{padding:0;font-size:12px;text-align:center}.gist .blob-num-expandable .diff-expander{display:block;width:auto;height:auto;padding:4px 11px 4px 10px;margin-right:-1px;color:#586069;cursor:pointer}.gist .blob-num-expandable .diff-expander .octicon{vertical-align:top}.gist .blob-num-expandable .directional-expander{display:block;width:auto;height:auto;margin-right:-1px;color:#586069;cursor:pointer}.gist .blob-num-expandable .single-expander{padding-top:4px;padding-bottom:4px}.gist .blob-num-expandable .diff-expander:hover,.gist .blob-num-expandable .directional-expander:hover{color:#fff;text-shadow:none;background-color:#0366d6;border-color:#0366d6}.gist .blob-code-addition{background-color:#e6ffed}.gist .blob-code-addition .x{color:#24292e;background-color:#acf2bd}.gist .blob-num-addition{background-color:#cdffd8;border-color:#bef5cb}.gist .blob-code-deletion{background-color:#ffeef0}.gist .blob-code-deletion .x{color:#24292e;background-color:#fdb8c0}.gist .blob-num-deletion{background-color:#ffdce0;border-color:#fdaeb7}.gist .is-selecting,.gist .is-selecting .blob-num{cursor:ns-resize !important}.gist .is-selecting .add-line-comment,.gist .is-selecting a{pointer-events:none;cursor:ns-resize !important}.gist .is-selecting .is-hovered .add-line-comment{opacity:0}.gist .is-selecting.file-diff-split,.gist .is-selecting.file-diff-split .blob-num{cursor:nwse-resize !important}.gist .is-selecting.file-diff-split .add-line-comment,.gist .is-selecting.file-diff-split .empty-cell,.gist .is-selecting.file-diff-split a{pointer-events:none;cursor:nwse-resize !important}.gist .selected-line{position:relative}.gist .selected-line:after{position:absolute;top:0;left:0;display:block;width:100%;height:100%;box-sizing:border-box;pointer-events:none;content:"";background:rgba(255,223,93,.2);mix-blend-mode:multiply}.gist .selected-line.selected-line-top:after{border-top:1px solid #ffd33d}.gist .selected-line.selected-line-bottom:after{border-bottom:1px solid #ffd33d}.gist .selected-line.selected-line-left:after,.gist .selected-line:first-child:after{border-left:1px solid #ffd33d}.gist .selected-line.selected-line-right:after,.gist .selected-line:last-child:after{border-right:1px solid #ffd33d}.gist .is-commenting .selected-line.blob-code:before{position:absolute;top:0;left:-1px;display:block;width:4px;height:100%;content:"";background:#0366d6}.gist .add-line-comment{position:relative;z-index:5;float:left;width:22px;height:22px;margin:-2px -10px -2px -20px;line-height:21px;color:#fff;text-align:center;text-indent:0;cursor:pointer;background-color:#0366d6;background-image:linear-gradient(#0372ef,#0366d6);border-radius:3px;box-shadow:0 1px 4px rgba(27,31,35,.15);opacity:0;transition:transform .1s ease-in-out;transform:scale(.8)}.gist .add-line-comment:hover{transform:scale(1)}.gist .add-line-comment:focus,.is-hovered .gist .add-line-comment{opacity:1}.gist .add-line-comment .octicon{vertical-align:text-top;pointer-events:none}.gist .add-line-comment.octicon-check{background:#333;opacity:1}.gist .inline-comment-form{border:1px solid #dfe2e5;border-radius:3px}.gist .inline-review-comment{margin-top:0 !important;margin-bottom:10px !important}.gist .inline-review-comment .gc:first-child+tr .blob-code,.gist .inline-review-comment .gc:first-child+tr .blob-num{padding-top:5px}.gist .inline-review-comment tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.gist .inline-review-comment tr:last-child .blob-code,.gist .inline-review-comment tr:last-child .blob-num{padding-bottom:8px}.gist .inline-review-comment tr:last-child .blob-code:first-child,.gist .inline-review-comment tr:last-child .blob-num:first-child{border-bottom-left-radius:3px}.gist .inline-review-comment tr:last-child .blob-code:last-child,.gist .inline-review-comment tr:last-child .blob-num:last-child{border-bottom-right-radius:3px}.gist .timeline-inline-comments{width:100%;table-layout:fixed}.gist .show-inline-notes .inline-comments,.gist .timeline-inline-comments .inline-comments{display:table-row}.gist .inline-comments,.gist .inline-comments.is-collapsed{display:none}.gist .inline-comments .line-comments.is-collapsed{visibility:hidden}.gist .inline-comments .line-comments+.blob-num{border-left-width:1px}.gist .inline-comments .timeline-comment{margin-bottom:10px}.gist .comment-holder,.gist .inline-comments .inline-comment-form,.gist .inline-comments .inline-comment-form-container{max-width:780px}.gist .empty-cell+.line-comments,.gist .line-comments+.line-comments{border-left:1px solid #eaecef}.gist .inline-comment-form-container .inline-comment-form,.gist .inline-comment-form-container.open .inline-comment-form-actions{display:none}.gist .inline-comment-form-container .inline-comment-form-actions,.gist .inline-comment-form-container.open .inline-comment-form{display:block}.gist body.full-width .container,.gist body.full-width .container-lg,.gist body.full-width .container-xl,.gist body.split-diff .container,.gist body.split-diff .container-lg,.gist body.split-diff .container-xl{width:100%;max-width:none;padding-right:20px;padding-left:20px}.gist body.full-width .repository-content,.gist body.split-diff .repository-content{width:100%}.gist body.full-width .new-pr-form,.gist body.split-diff .new-pr-form{max-width:980px}.gist .file-diff-split{table-layout:fixed}.gist .file-diff-split .blob-code+.blob-num{border-left:1px solid #f6f8fa}.gist .file-diff-split .blob-code-inner{word-wrap:break-word;white-space:pre-wrap}.gist .file-diff-split .empty-cell{cursor:default;background-color:#fafbfc;border-right-color:#eaecef}@media (max-width:1280px){.gist .file-diff-split .write-selected .comment-form-head{margin-bottom:48px !important}.gist .file-diff-split markdown-toolbar{position:absolute;right:8px;bottom:-40px}}.gist .submodule-diff-stats .octicon-diff-removed{color:#cb2431}.gist .submodule-diff-stats .octicon-diff-renamed{color:#677a85}.gist .submodule-diff-stats .octicon-diff-modified{color:#d0b44c}.gist .submodule-diff-stats .octicon-diff-added{color:#28a745}.gist .BlobToolbar{left:-17px}.gist .BlobToolbar-dropdown{margin-left:-2px}.gist .code-navigation-banner{background:linear-gradient(180deg,rgba(242,248,254,0),rgba(242,248,254,.47))}.gist .code-navigation-banner .code-navigation-banner-illo{background-image:url(code-navigation-banner-illo.svg);background-repeat:no-repeat;background-position:50%}.gist .pl-token.active,.gist .pl-token:hover{cursor:pointer;background:#ffea7f}.gist .task-list-item{list-style-type:none}.gist .task-list-item label{font-weight:400}.gist .task-list-item.enabled label{cursor:pointer}.gist .task-list-item+.task-list-item{margin-top:3px}.gist .task-list-item .handle{display:none}.gist .task-list-item-checkbox{margin:0 .2em .25em -1.6em;vertical-align:middle}.gist .reorderable-task-lists .markdown-body .contains-task-list{padding:0}.gist .reorderable-task-lists .markdown-body li:not(.task-list-item){margin-left:26px}.gist .reorderable-task-lists .markdown-body ol:not(.contains-task-list) li,.gist .reorderable-task-lists .markdown-body ul:not(.contains-task-list) li{margin-left:0}.gist .reorderable-task-lists .markdown-body li p{margin-top:0}.gist .reorderable-task-lists .markdown-body .task-list-item{padding-right:15px;padding-left:42px;margin-right:-15px;margin-left:-15px;border:1px solid transparent}.gist .reorderable-task-lists .markdown-body .task-list-item+.task-list-item{margin-top:0}.gist .reorderable-task-lists .markdown-body .task-list-item .contains-task-list{padding-top:4px}.gist .reorderable-task-lists .markdown-body .task-list-item .handle{display:block;float:left;width:20px;padding:2px 0 0 2px;margin-left:-43px;opacity:0}.gist .reorderable-task-lists .markdown-body .task-list-item .drag-handle{fill:#333}.gist .reorderable-task-lists .markdown-body .task-list-item.hovered>.handle{opacity:1}.gist .reorderable-task-lists .markdown-body .task-list-item.is-dragging{opacity:0}.gist .review-comment-contents .markdown-body .task-list-item{padding-left:42px;margin-right:-12px;margin-left:-12px;border-top-left-radius:3px;border-bottom-left-radius:3px}.gist .review-comment-contents .markdown-body .task-list-item.hovered{border-left-color:#ededed}.gist .highlight{padding:0;margin:0;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px;font-weight:400;line-height:1.4;color:#333;background:#fff;border:0}.gist .octospinner,.gist .render-viewer-error,.gist .render-viewer-fatal,.gist .render-viewer-invalid{display:none}.gist iframe.render-viewer{width:100%;height:480px;overflow:hidden;border:0}.gist code,.gist pre{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace !important}.gist .gist-meta{padding:10px;overflow:hidden;font:12px -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;color:#586069;background-color:#f7f7f7;border-radius:0 0 3px 3px}.gist .gist-meta a{font-weight:600;color:#666;text-decoration:none;border:0}.gist .gist-data{overflow:auto;word-wrap:normal;background-color:#fff;border-bottom:1px solid #ddd;border-radius:3px 3px 0 0}.gist .gist-file{margin-bottom:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:3px}.gist .gist-file article{padding:6px}.gist .gist-file .scroll .gist-data{position:absolute;top:0;right:0;bottom:30px;left:0;overflow:scroll}.gist .gist-file .scroll .gist-meta{position:absolute;right:0;bottom:0;left:0}.gist .blob-num{min-width:inherit}.gist .blob-code,.gist .blob-num{padding:1px 10px !important;background:transparent}.gist .blob-code{text-align:left;border:0}.gist .blob-wrapper table{border-collapse:collapse}.gist table,.gist table tr,.gist table tr td,.gist table tr th{border-collapse:collapse}.gist .blob-wrapper tr:first-child td{padding-top:4px}.gist .markdown-body .anchor{display:none}
--------------------------------------------------------------------------------
/static/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pedroherpeto/wuzapi/e1d689e33dcdf9b6d68fd5d6d074fe39fc4d0ab3/static/images/favicon.png
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WuzAPI - API REST para WhatsApp
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
31 |
32 |
33 |
34 |
WuzAPI
35 |
WuzAPI é uma implementação da biblioteca @tulir/whatsmeow como um serviço RESTful API com suporte a múltiplos dispositivos e sessões concorrentes.
36 |
37 |
O Whatsmeow não usa Puppeteer em Chrome headless, nem um emulador Android. Ele se comunica diretamente com os servidores websocket do WhatsApp, sendo assim bastante rápido e usando muito menos memória e CPU que essas soluções. A desvantagem é que uma mudança no protocolo do WhatsApp pode quebrar as conexões e exigir uma atualização da biblioteca.
38 |
39 |
⚠️ Aviso
40 |
Usar este software violando os Termos de Serviço do WhatsApp pode resultar no banimento do seu número : Tenha muito cuidado, não use para enviar SPAM ou qualquer coisa do tipo. Use por sua conta e risco. Se você precisa desenvolver algo com interesse comercial, você deve entrar em contato com um provedor global de soluções WhatsApp e se inscrever no serviço WhatsApp Business API.
41 |
42 |
Endpoints Disponíveis
43 |
44 | Sessão: conectar, desconectar e fazer logout do WhatsApp. Recuperar status de conexão. Recuperar código QR para escanear.
45 | Mensagens: enviar texto, imagem, áudio, documento, template, vídeo, adesivo, localização e mensagens de contato.
46 | Usuários: verificar se os telefones têm WhatsApp, obter informações do usuário, obter avatar do usuário, recuperar lista completa de contatos.
47 | Chat: definir presença (digitando/pausado, gravando mídia), marcar mensagens como lidas, baixar imagens das mensagens.
48 | Grupos: listar inscritos, obter informações, obter links de convite, alterar foto e nome.
49 | Webhooks: definir e obter webhook que será chamado sempre que um evento/mensagem for recebido.
50 |
51 |
52 |
Pré-requisitos
53 |
Pacotes:
54 |
55 | Go (Linguagem de Programação Go)
56 |
57 |
Opcional:
58 |
59 | Docker (Containerização)
60 |
61 |
62 |
Compilando
63 |
go build .
64 |
65 |
Executando
66 |
Por padrão, iniciará um serviço REST na porta 8080. Estes são os parâmetros que você pode usar para alterar o comportamento:
67 |
68 | -address: define o endereço IP para vincular o servidor (padrão 0.0.0.0)
69 | -port: define o número da porta (padrão 8080)
70 | -logtype: formato para logs, console (padrão) ou json
71 | -wadebug: ativar debug do whatsmeow, níveis INFO ou DEBUG são suportados
72 | -sslcertificate: Arquivo de Certificado SSL
73 | -sslprivatekey: Arquivo de Chave Privada SSL
74 |
75 |
Exemplo:
76 |
./wuzapi -logtype json
77 |
78 |
Uso
79 |
Para abrir sessões, primeiro você precisa criar um usuário e definir um token de autenticação para ele. Você pode fazer isso atualizando o banco de dados Postgres users.db :
80 |
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "INSERT INTO users (name, token) VALUES ('John','1234ABCD');"
81 |
82 |
Depois de criar alguns usuários, você pode falar com a API passando o cabeçalho Token como um meio simples de autenticação. Você pode ter vários usuários (números diferentes) no mesmo servidor.
83 |
84 |
O daemon também serve alguns arquivos web estáticos, úteis para desenvolvimento/teste que você pode carregar com seu navegador:
85 |
86 | Uma referência de API swagger em /api
87 | Uma página web de exemplo para conectar e escanear códigos QR em /login (onde você precisará passar ?token=1234ABCD)
88 |
89 |
90 |
Referência da API
91 |
As chamadas de API devem ser feitas com o tipo de conteúdo json, e os parâmetros enviados no corpo da requisição, sempre passando o cabeçalho Token para autenticar a requisição.
92 |
Verifique a Referência da API Swagger
93 |
94 |
Licença
95 |
Copyright © 2022 Nicolás Gudiño
96 |
MIT
97 |
98 |
Atribuição do Ícone
99 |
Ícones de comunicação criados por Vectors Market - Flaticon
100 |
101 |
Legal
102 |
Este código não é de forma alguma afiliado, autorizado, mantido, patrocinado ou endossado pelo WhatsApp ou qualquer de suas afiliadas ou subsidiárias. Este é um software independente e não oficial. Use por sua conta e risco.
103 |
104 |
Aviso de Criptografia
105 |
Esta distribuição inclui software criptográfico. O país em que você atualmente reside pode ter restrições sobre a importação, posse, uso e/ou reexportação para outro país, de software de criptografia. ANTES de usar qualquer software de criptografia, por favor verifique as leis, regulamentos e políticas do seu país relativas à importação, posse ou uso, e reexportação de software de criptografia, para ver se isso é permitido. Veja http://www.wassenaar.org/ para mais informações.
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/static/login/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | WuzAPI
8 |
9 |
10 |
11 |
12 |
13 |
Bem-vindo ao WuzAPI
14 |
15 |
16 |
17 |
18 |
19 | Para acessar esta página, inclua seu token na URL: ?token=seu_token_aqui
20 |
21 |
22 | Por padrão, o sistema cria um usuário "admin" com o token definido na variável de ambiente WUZAPI_ADMIN_TOKEN.
23 |
24 |
25 |
26 |
27 |
28 |
Por favor, escaneie este código QR com o WhatsApp para habilitar a API
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/static/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: #25d366;
3 | --secondary-color: #128c7e;
4 | --dark-color: #075e54;
5 | --light-color: #dcf8c6;
6 | --accent-color: #34b7f1;
7 | --text-color: #333;
8 | --light-text: #f5f5f5;
9 | --gray-bg: #f5f5f5;
10 | --dark-bg: #1a1a1a;
11 | --card-shadow: 0 8px 20px rgba(0,0,0,0.1);
12 | --hover-shadow: 0 12px 28px rgba(0,0,0,0.2);
13 | --transition-normal: all 0.3s ease;
14 | --transition-smooth: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
15 | }
16 |
17 | * {
18 | margin: 0;
19 | padding: 0;
20 | box-sizing: border-box;
21 | }
22 |
23 | body {
24 | font-family: 'Poppins', sans-serif;
25 | line-height: 1.6;
26 | color: var(--text-color);
27 | background-color: #fff;
28 | overflow-x: hidden;
29 | }
30 |
31 | .container {
32 | max-width: 1200px;
33 | margin: 0 auto;
34 | padding: 0 2rem;
35 | }
36 |
37 | /* Header */
38 | header {
39 | background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
40 | color: white;
41 | padding: 1rem 0;
42 | position: fixed;
43 | width: 100%;
44 | top: 0;
45 | z-index: 1000;
46 | box-shadow: 0 4px 12px rgba(0,0,0,0.15);
47 | }
48 |
49 | .header-container {
50 | max-width: 1200px;
51 | margin: 0 auto;
52 | padding: 0 2rem;
53 | display: flex;
54 | justify-content: space-between;
55 | align-items: center;
56 | }
57 |
58 | .logo {
59 | display: flex;
60 | align-items: center;
61 | gap: 1rem;
62 | }
63 |
64 | .logo img {
65 | width: 40px;
66 | height: auto;
67 | filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
68 | }
69 |
70 | .logo h1 {
71 | font-size: 1.5rem;
72 | font-weight: 700;
73 | background: linear-gradient(90deg, #fff, #dcf8c6);
74 | -webkit-background-clip: text;
75 | background-clip: text;
76 | -webkit-text-fill-color: transparent;
77 | }
78 |
79 | /* Main Content */
80 | main {
81 | padding-top: 80px;
82 | min-height: 100vh;
83 | }
84 |
85 | .markdown-body {
86 | max-width: 1200px;
87 | margin: 0 auto;
88 | padding: 2rem;
89 | background: white;
90 | border-radius: 8px;
91 | box-shadow: var(--card-shadow);
92 | }
93 |
94 | .markdown-body h1 {
95 | color: var(--dark-color);
96 | font-size: 2.5rem;
97 | margin-bottom: 2rem;
98 | padding-bottom: 1rem;
99 | border-bottom: 2px solid var(--light-color);
100 | }
101 |
102 | .markdown-body h2 {
103 | color: var(--secondary-color);
104 | font-size: 2rem;
105 | margin: 2rem 0 1rem;
106 | }
107 |
108 | .markdown-body p {
109 | margin-bottom: 1rem;
110 | line-height: 1.8;
111 | }
112 |
113 | .markdown-body ul {
114 | margin: 1rem 0;
115 | padding-left: 2rem;
116 | }
117 |
118 | .markdown-body li {
119 | margin-bottom: 0.5rem;
120 | }
121 |
122 | .markdown-body code {
123 | background-color: var(--gray-bg);
124 | padding: 0.2rem 0.4rem;
125 | border-radius: 4px;
126 | font-family: 'Fira Code', monospace;
127 | font-size: 0.9em;
128 | }
129 |
130 | .markdown-body pre {
131 | background-color: var(--dark-bg);
132 | color: var(--light-text);
133 | padding: 1rem;
134 | border-radius: 8px;
135 | overflow-x: auto;
136 | margin: 1rem 0;
137 | }
138 |
139 | .markdown-body pre code {
140 | background-color: transparent;
141 | padding: 0;
142 | color: inherit;
143 | }
144 |
145 | /* Links */
146 | .markdown-body a {
147 | color: var(--accent-color);
148 | text-decoration: none;
149 | transition: var(--transition-normal);
150 | }
151 |
152 | .markdown-body a:hover {
153 | color: var(--secondary-color);
154 | text-decoration: underline;
155 | }
156 |
157 | /* Warning Box */
158 | .markdown-body h2#user-content-ezf3weaa2uhg9u52fwkic-warning {
159 | background-color: #fff3cd;
160 | color: #856404;
161 | padding: 1rem;
162 | border-radius: 8px;
163 | border-left: 4px solid #ffeeba;
164 | }
165 |
166 | /* Responsive Design */
167 | @media (max-width: 768px) {
168 | .header-container {
169 | padding: 0 1rem;
170 | }
171 |
172 | .markdown-body {
173 | padding: 1rem;
174 | margin: 1rem;
175 | }
176 |
177 | .markdown-body h1 {
178 | font-size: 2rem;
179 | }
180 |
181 | .markdown-body h2 {
182 | font-size: 1.5rem;
183 | }
184 | }
--------------------------------------------------------------------------------
/wmiau.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "encoding/base64"
7 | "encoding/json"
8 | "errors"
9 | "fmt"
10 | "mime"
11 | "net/http"
12 | "os"
13 | "path/filepath"
14 | "strconv"
15 | "strings"
16 | "sync/atomic"
17 | "time"
18 |
19 | "regexp"
20 |
21 | "github.com/go-resty/resty/v2"
22 | "github.com/jmoiron/sqlx" // Importação do sqlx
23 | "github.com/mdp/qrterminal/v3"
24 | "github.com/patrickmn/go-cache"
25 | "github.com/rs/zerolog/log"
26 | "github.com/skip2/go-qrcode"
27 | "go.mau.fi/whatsmeow"
28 | "go.mau.fi/whatsmeow/appstate"
29 | waProto "go.mau.fi/whatsmeow/binary/proto"
30 | "go.mau.fi/whatsmeow/store"
31 | "go.mau.fi/whatsmeow/types"
32 | "go.mau.fi/whatsmeow/types/events"
33 | waLog "go.mau.fi/whatsmeow/util/log"
34 | )
35 |
36 | // var wlog waLog.Logger
37 | var clientPointer = make(map[int]*whatsmeow.Client)
38 | var clientHttp = make(map[int]*resty.Client)
39 | var historySyncID int32
40 |
41 | // Declaração do campo db como *sqlx.DB
42 | type MyClient struct {
43 | WAClient *whatsmeow.Client
44 | eventHandlerID uint32
45 | userID int
46 | token string
47 | subscriptions []string
48 | db *sqlx.DB
49 | }
50 |
51 | // Connects to Whatsapp Websocket on server startup if last state was connected
52 | func (s *server) connectOnStartup() {
53 | rows, err := s.db.Queryx("SELECT id,token,jid,webhook,events FROM users WHERE connected=1")
54 | if err != nil {
55 | log.Error().Err(err).Msg("DB Problem")
56 | return
57 | }
58 | defer rows.Close()
59 | for rows.Next() {
60 | txtid := ""
61 | token := ""
62 | jid := ""
63 | webhook := ""
64 | events := ""
65 | err = rows.Scan(&txtid, &token, &jid, &webhook, &events)
66 | if err != nil {
67 | log.Error().Err(err).Msg("DB Problem")
68 | return
69 | } else {
70 | log.Info().Str("token", token).Msg("Connect to Whatsapp on startup")
71 | v := Values{map[string]string{
72 | "Id": txtid,
73 | "Jid": jid,
74 | "Webhook": webhook,
75 | "Token": token,
76 | "Events": events,
77 | }}
78 | userinfocache.Set(token, v, cache.NoExpiration)
79 | userid, _ := strconv.Atoi(txtid)
80 | // Gets and set subscription to webhook events
81 | eventarray := strings.Split(events, ",")
82 |
83 | var subscribedEvents []string
84 | if len(eventarray) < 1 {
85 | if !Find(subscribedEvents, "All") {
86 | subscribedEvents = append(subscribedEvents, "All")
87 | }
88 | } else {
89 | for _, arg := range eventarray {
90 | if !Find(messageTypes, arg) {
91 | log.Warn().Str("Type", arg).Msg("Message type discarded")
92 | continue
93 | }
94 | if !Find(subscribedEvents, arg) {
95 | subscribedEvents = append(subscribedEvents, arg)
96 | }
97 | }
98 | }
99 | eventstring := strings.Join(subscribedEvents, ",")
100 | log.Info().Str("events", eventstring).Str("jid", jid).Msg("Attempt to connect")
101 | killchannel[userid] = make(chan bool)
102 | go s.startClient(userid, jid, token, subscribedEvents)
103 | }
104 | }
105 | err = rows.Err()
106 | if err != nil {
107 | log.Error().Err(err).Msg("DB Problem")
108 | }
109 | }
110 |
111 | func parseJID(arg string) (types.JID, bool) {
112 | if arg[0] == '+' {
113 | arg = arg[1:]
114 | }
115 | if !strings.ContainsRune(arg, '@') {
116 | return types.NewJID(arg, types.DefaultUserServer), true
117 | } else {
118 | recipient, err := types.ParseJID(arg)
119 | if err != nil {
120 | log.Error().Err(err).Msg("Invalid JID")
121 | return recipient, false
122 | } else if recipient.User == "" {
123 | log.Error().Err(err).Msg("Invalid JID no server specified")
124 | return recipient, false
125 | }
126 | return recipient, true
127 | }
128 | }
129 |
130 | func (s *server) startClient(userID int, textjid string, token string, subscriptions []string) {
131 |
132 | log.Info().Str("userid", strconv.Itoa(userID)).Str("jid", textjid).Msg("Starting websocket connection to Whatsapp")
133 |
134 | var deviceStore *store.Device
135 | var err error
136 |
137 | if clientPointer[userID] != nil {
138 | isConnected := clientPointer[userID].IsConnected()
139 | if isConnected == true {
140 | return
141 | }
142 | }
143 |
144 | if textjid != "" {
145 | jid, _ := parseJID(textjid)
146 | // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
147 | //deviceStore, err := container.GetFirstDevice()
148 | deviceStore, err = container.GetDevice(jid)
149 | if err != nil {
150 | panic(err)
151 | }
152 | } else {
153 | log.Warn().Msg("No jid found. Creating new device")
154 | deviceStore = container.NewDevice()
155 | }
156 |
157 | if deviceStore == nil {
158 | log.Warn().Msg("No store found. Creating new one")
159 | deviceStore = container.NewDevice()
160 | }
161 |
162 | //store.CompanionProps.PlatformType = waProto.CompanionProps_CHROME.Enum()
163 | //store.CompanionProps.Os = proto.String("Mac OS")
164 |
165 | osName := "Mac OS 10"
166 | store.DeviceProps.PlatformType = waProto.DeviceProps_UNKNOWN.Enum()
167 | store.DeviceProps.Os = &osName
168 |
169 | clientLog := waLog.Stdout("Client", *waDebug, *colorOutput)
170 | var client *whatsmeow.Client
171 | if *waDebug != "" {
172 | client = whatsmeow.NewClient(deviceStore, clientLog)
173 | } else {
174 | client = whatsmeow.NewClient(deviceStore, nil)
175 | }
176 | clientPointer[userID] = client
177 | mycli := MyClient{client, 1, userID, token, subscriptions, s.db}
178 | mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler)
179 |
180 | //clientHttp[userID] = resty.New().EnableTrace()
181 | clientHttp[userID] = resty.New()
182 | clientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
183 | if *waDebug == "DEBUG" {
184 | clientHttp[userID].SetDebug(true)
185 | }
186 | clientHttp[userID].SetTimeout(30 * time.Second)
187 | clientHttp[userID].SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
188 | clientHttp[userID].OnError(func(req *resty.Request, err error) {
189 | if v, ok := err.(*resty.ResponseError); ok {
190 | // v.Response contains the last response from the server
191 | // v.Err contains the original error
192 | log.Debug().Str("response", v.Response.String()).Msg("resty error")
193 | log.Error().Err(v.Err).Msg("resty error")
194 | }
195 | })
196 |
197 | // NEW: set proxy if defined in DB (assumes users table contains proxy_url column)
198 | var proxyURL string
199 | err = s.db.Get(&proxyURL, "SELECT proxy_url FROM users WHERE id=$1", userID)
200 | if err == nil && proxyURL != "" {
201 | clientHttp[userID].SetProxy(proxyURL)
202 | }
203 |
204 | if client.Store.ID == nil {
205 | // No ID stored, new login
206 |
207 | qrChan, err := client.GetQRChannel(context.Background())
208 | if err != nil {
209 | // This error means that we're already logged in, so ignore it.
210 | if !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
211 | log.Error().Err(err).Msg("Failed to get QR channel")
212 | }
213 | } else {
214 | err = client.Connect() // Si no conectamos no se puede generar QR
215 | if err != nil {
216 | panic(err)
217 | }
218 | for evt := range qrChan {
219 | if evt.Event == "code" {
220 | // Display QR code in terminal (useful for testing/developing)
221 | if *logType != "json" {
222 | qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
223 | fmt.Println("QR code:\n", evt.Code)
224 | }
225 | // Store encoded/embeded base64 QR on database for retrieval with the /qr endpoint
226 | image, _ := qrcode.Encode(evt.Code, qrcode.Medium, 256)
227 | base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
228 | sqlStmt := `UPDATE users SET qrcode=$1 WHERE id=$2`
229 | _, err := s.db.Exec(sqlStmt, base64qrcode, userID)
230 | if err != nil {
231 | log.Error().Err(err).Msg(sqlStmt)
232 | }
233 | } else if evt.Event == "timeout" {
234 | // Clear QR code from DB on timeout
235 | sqlStmt := `UPDATE users SET qrcode=$1 WHERE id=$2`
236 | _, err := s.db.Exec(sqlStmt, "", userID)
237 | if err != nil {
238 | log.Error().Err(err).Msg(sqlStmt)
239 | }
240 | log.Warn().Msg("QR timeout killing channel")
241 | delete(clientPointer, userID)
242 | killchannel[userID] <- true
243 | } else if evt.Event == "success" {
244 | log.Info().Msg("QR pairing ok!")
245 | // Clear QR code after pairing
246 | sqlStmt := `UPDATE users SET qrcode=$1, connected=1 WHERE id=$2`
247 | _, err := s.db.Exec(sqlStmt, "", userID)
248 | if err != nil {
249 | log.Error().Err(err).Msg(sqlStmt)
250 | }
251 | } else {
252 | log.Info().Str("event", evt.Event).Msg("Login event")
253 | }
254 | }
255 | }
256 |
257 | } else {
258 | // Already logged in, just connect
259 | log.Info().Msg("Already logged in, just connect")
260 | err = client.Connect()
261 | if err != nil {
262 | panic(err)
263 | }
264 | }
265 |
266 | // Keep connected client live until disconnected/killed
267 | for {
268 | select {
269 | case <-killchannel[userID]:
270 | log.Info().Str("userid", strconv.Itoa(userID)).Msg("Received kill signal")
271 | client.Disconnect()
272 | delete(clientPointer, userID)
273 | sqlStmt := `UPDATE users SET qrcode=$1 connected=0 WHERE id=$1`
274 | _, err := s.db.Exec(sqlStmt, "", userID)
275 | if err != nil {
276 | log.Error().Err(err).Msg(sqlStmt)
277 | }
278 | return
279 | default:
280 | time.Sleep(1000 * time.Millisecond)
281 | //log.Info().Str("jid",textjid).Msg("Loop the loop")
282 | }
283 | }
284 | }
285 |
286 | // Função para remover conteúdo Base64 de um mapa
287 |
288 | func filterBase64Data(input map[string]interface{}) map[string]interface{} {
289 | base64Pattern := `^data:[\w/\-]+;base64,`
290 | filtered := make(map[string]interface{})
291 |
292 | for k, v := range input {
293 | strVal, ok := v.(string)
294 | // Remover se a chave for "base64" ou o valor corresponder ao padrão Base64
295 | if k == "base64" || (ok && regexp.MustCompile(base64Pattern).MatchString(strVal)) {
296 | continue // Ignorar a propriedade Base64 completa
297 | }
298 | filtered[k] = v
299 | }
300 |
301 | return filtered
302 | }
303 |
304 | func fileToBase64(filepath string) (string, string, error) {
305 | data, err := os.ReadFile(filepath)
306 | if err != nil {
307 | return "", "", err
308 | }
309 | mimeType := http.DetectContentType(data)
310 | return base64.StdEncoding.EncodeToString(data), mimeType, nil
311 | }
312 |
313 | func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
314 | txtid := strconv.Itoa(mycli.userID)
315 | postmap := make(map[string]interface{})
316 | postmap["event"] = rawEvt
317 | dowebhook := 0
318 | path := ""
319 |
320 | ex, err := os.Executable()
321 | if err != nil {
322 | panic(err)
323 | }
324 | exPath := filepath.Dir(ex)
325 |
326 | switch evt := rawEvt.(type) {
327 | case *events.AppStateSyncComplete:
328 | if len(mycli.WAClient.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
329 | err := mycli.WAClient.SendPresence(types.PresenceAvailable)
330 | if err != nil {
331 | log.Warn().Err(err).Msg("Failed to send available presence")
332 | } else {
333 | log.Info().Msg("Marked self as available")
334 | }
335 | }
336 | case *events.Connected, *events.PushNameSetting:
337 | if len(mycli.WAClient.Store.PushName) == 0 {
338 | return
339 | }
340 | // Send presence available when connecting and when the pushname is changed.
341 | // This makes sure that outgoing messages always have the right pushname.
342 | err := mycli.WAClient.SendPresence(types.PresenceAvailable)
343 | if err != nil {
344 | log.Warn().Err(err).Msg("Failed to send available presence")
345 | } else {
346 | log.Info().Msg("Marked self as available")
347 | }
348 | sqlStmt := `UPDATE users SET connected=1 WHERE id=$1`
349 | _, err = mycli.db.Exec(sqlStmt, mycli.userID)
350 | if err != nil {
351 | log.Error().Err(err).Msg(sqlStmt)
352 | return
353 | }
354 | case *events.PairSuccess:
355 | log.Info().Str("userid", strconv.Itoa(mycli.userID)).Str("token", mycli.token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success")
356 | jid := evt.ID
357 | sqlStmt := `UPDATE users SET jid=$1 WHERE id=$2`
358 | _, err := mycli.db.Exec(sqlStmt, jid, mycli.userID)
359 | if err != nil {
360 | log.Error().Err(err).Msg(sqlStmt)
361 | return
362 | }
363 |
364 | myuserinfo, found := userinfocache.Get(mycli.token)
365 | if !found {
366 | log.Warn().Msg("No user info cached on pairing?")
367 | } else {
368 | txtid := myuserinfo.(Values).Get("Id")
369 | token := myuserinfo.(Values).Get("Token")
370 | v := updateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid))
371 | userinfocache.Set(token, v, cache.NoExpiration)
372 | log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set")
373 | }
374 | case *events.StreamReplaced:
375 | log.Info().Msg("Received StreamReplaced event")
376 | return
377 | case *events.Message:
378 | postmap["type"] = "Message"
379 | dowebhook = 1
380 | metaParts := []string{fmt.Sprintf("pushname: %s", evt.Info.PushName), fmt.Sprintf("timestamp: %s", evt.Info.Timestamp)}
381 | if evt.Info.Type != "" {
382 | metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type))
383 | }
384 | if evt.Info.Category != "" {
385 | metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category))
386 | }
387 | if evt.IsViewOnce {
388 | metaParts = append(metaParts, "view once")
389 | }
390 | if evt.IsViewOnce {
391 | metaParts = append(metaParts, "ephemeral")
392 | }
393 |
394 | log.Info().Str("id", evt.Info.ID).Str("source", evt.Info.SourceString()).Str("parts", strings.Join(metaParts, ", ")).Msg("Message Received")
395 |
396 | // try to get Image if any
397 | img := evt.Message.GetImageMessage()
398 | if img != nil {
399 | // check/creates user directory for files
400 | userDirectory := filepath.Join(exPath, "files", "user_"+txtid)
401 | _, err := os.Stat(userDirectory)
402 | if os.IsNotExist(err) {
403 | errDir := os.MkdirAll(userDirectory, 0751)
404 | if errDir != nil {
405 | log.Error().Err(errDir).Msg("Could not create user directory")
406 | return
407 | }
408 | }
409 |
410 | data, err := mycli.WAClient.Download(img)
411 | if err != nil {
412 | log.Error().Err(err).Msg("Failed to download image")
413 | return
414 | }
415 | exts, _ := mime.ExtensionsByType(img.GetMimetype())
416 | path = filepath.Join(userDirectory, evt.Info.ID+exts[0])
417 | err = os.WriteFile(path, data, 0600)
418 | if err != nil {
419 | log.Error().Err(err).Msg("Failed to save image")
420 | return
421 | }
422 | log.Info().Str("path", path).Msg("Image saved")
423 | // Converte a imagem para base64
424 | base64String, mimeType, err := fileToBase64(path)
425 | if err == nil {
426 | postmap["base64"] = base64String
427 | postmap["mimeType"] = mimeType
428 | postmap["fileName"] = filepath.Base(path)
429 | } else {
430 | log.Error().Err(err).Msg("Failed to convert image to base64")
431 | }
432 | // log.Debug().Str("path",path).Msg("Image converted to base64")
433 | }
434 |
435 | // try to get Audio if any
436 | audio := evt.Message.GetAudioMessage()
437 | if audio != nil {
438 | // check/creates user directory for files
439 | userDirectory := filepath.Join(exPath, "files", "user_"+txtid)
440 | _, err := os.Stat(userDirectory)
441 | if os.IsNotExist(err) {
442 | errDir := os.MkdirAll(userDirectory, 0751)
443 | if errDir != nil {
444 | log.Error().Err(errDir).Msg("Could not create user directory")
445 | return
446 | }
447 | }
448 |
449 | data, err := mycli.WAClient.Download(audio)
450 | if err != nil {
451 | log.Error().Err(err).Msg("Failed to download audio")
452 | return
453 | }
454 | exts, _ := mime.ExtensionsByType(audio.GetMimetype())
455 | var ext string
456 | if len(exts) > 0 {
457 | ext = exts[0]
458 | } else {
459 | ext = ".ogg"
460 | }
461 | path = filepath.Join(userDirectory, evt.Info.ID+ext)
462 | err = os.WriteFile(path, data, 0600)
463 | if err != nil {
464 | log.Error().Err(err).Msg("Failed to save audio")
465 | return
466 | }
467 | log.Info().Str("path", path).Msg("Audio saved")
468 | // Converte o áudio para base64
469 | base64String, mimeType, err := fileToBase64(path)
470 | if err == nil {
471 | postmap["base64"] = base64String
472 | postmap["mimeType"] = mimeType
473 | postmap["fileName"] = filepath.Base(path)
474 | } else {
475 | log.Error().Err(err).Msg("Failed to convert audio to base64")
476 | }
477 | // log.Debug().Str("path",path).Msg("Audio converted to base64")
478 | }
479 | // try to get Document if any
480 | document := evt.Message.GetDocumentMessage()
481 | if document != nil {
482 |
483 | // check/creates user directory for files
484 | userDirectory := filepath.Join(exPath, "files", "user_"+txtid)
485 | _, err := os.Stat(userDirectory)
486 | if os.IsNotExist(err) {
487 | errDir := os.MkdirAll(userDirectory, 0751)
488 | if errDir != nil {
489 | log.Error().Err(errDir).Msg("Could not create user directory")
490 | return
491 | }
492 | }
493 |
494 | data, err := mycli.WAClient.Download(document)
495 | if err != nil {
496 | log.Error().Err(err).Msg("Failed to download document")
497 | return
498 | }
499 | extension := ""
500 | exts, err := mime.ExtensionsByType(document.GetMimetype())
501 | if err != nil {
502 | extension = exts[0]
503 | } else {
504 | filename := document.FileName
505 | extension = filepath.Ext(*filename)
506 | }
507 | path = filepath.Join(userDirectory, evt.Info.ID+extension)
508 | err = os.WriteFile(path, data, 0600)
509 | if err != nil {
510 | log.Error().Err(err).Msg("Failed to save document")
511 | return
512 | }
513 | log.Info().Str("path", path).Msg("Document saved")
514 | // Converte o documento para base64
515 | base64String, mimeType, err := fileToBase64(path)
516 | if err == nil {
517 | postmap["base64"] = base64String
518 | postmap["mimeType"] = mimeType
519 | postmap["fileName"] = filepath.Base(path)
520 | } else {
521 | log.Error().Err(err).Msg("Failed to convert document to base64")
522 | }
523 | // log.Debug().Str("path",path).Msg("Document converted to base64")
524 | }
525 |
526 | // try to get Video if any
527 | video := evt.Message.GetVideoMessage()
528 | if video != nil {
529 | // check/creates user directory for files
530 | userDirectory := filepath.Join(exPath, "files", "user_"+txtid)
531 | _, err := os.Stat(userDirectory)
532 | if os.IsNotExist(err) {
533 | errDir := os.MkdirAll(userDirectory, 0751)
534 | if errDir != nil {
535 | log.Error().Err(errDir).Msg("Could not create user directory")
536 | return
537 | }
538 | }
539 |
540 | data, err := mycli.WAClient.Download(video)
541 | if err != nil {
542 | log.Error().Err(err).Msg("Failed to download video")
543 | return
544 | }
545 | exts, _ := mime.ExtensionsByType(video.GetMimetype())
546 | path = filepath.Join(userDirectory, evt.Info.ID+exts[0])
547 | err = os.WriteFile(path, data, 0600)
548 | if err != nil {
549 | log.Error().Err(err).Msg("Failed to save video")
550 | return
551 | }
552 | log.Info().Str("path", path).Msg("Video saved")
553 | // Converte o vídeo para base64
554 | base64String, mimeType, err := fileToBase64(path)
555 | if err == nil {
556 | postmap["base64"] = base64String
557 | postmap["mimeType"] = mimeType
558 | postmap["fileName"] = filepath.Base(path)
559 | } else {
560 | log.Error().Err(err).Msg("Failed to convert video to base64")
561 | }
562 | // log.Debug().Str("path",path).Msg("Video converted to base64")
563 | }
564 |
565 | case *events.Receipt:
566 | postmap["type"] = "ReadReceipt"
567 | dowebhook = 1
568 | if evt.Type == events.ReceiptTypeRead || evt.Type == events.ReceiptTypeReadSelf {
569 | log.Info().Strs("id", evt.MessageIDs).Str("source", evt.SourceString()).Str("timestamp", fmt.Sprintf("%d", evt.Timestamp)).Msg("Message was read")
570 | if evt.Type == events.ReceiptTypeRead {
571 | postmap["state"] = "Read"
572 | } else {
573 | postmap["state"] = "ReadSelf"
574 | }
575 | } else if evt.Type == events.ReceiptTypeDelivered {
576 | postmap["state"] = "Delivered"
577 | log.Info().Str("id", evt.MessageIDs[0]).Str("source", evt.SourceString()).Str("timestamp", fmt.Sprintf("%d", evt.Timestamp)).Msg("Message delivered")
578 | } else {
579 | // Discard webhooks for inactive or other delivery types
580 | return
581 | }
582 | case *events.Presence:
583 | postmap["type"] = "Presence"
584 | dowebhook = 1
585 | if evt.Unavailable {
586 | postmap["state"] = "offline"
587 | if evt.LastSeen.IsZero() {
588 | log.Info().Str("from", evt.From.String()).Msg("User is now offline")
589 | } else {
590 | log.Info().Str("from", evt.From.String()).Str("lastSeen", fmt.Sprintf("%d", evt.LastSeen)).Msg("User is now offline")
591 | }
592 | } else {
593 | postmap["state"] = "online"
594 | log.Info().Str("from", evt.From.String()).Msg("User is now online")
595 | }
596 | case *events.HistorySync:
597 | postmap["type"] = "HistorySync"
598 | dowebhook = 1
599 |
600 | // check/creates user directory for files
601 | userDirectory := filepath.Join(exPath, "files", "user_"+txtid)
602 | _, err := os.Stat(userDirectory)
603 | if os.IsNotExist(err) {
604 | errDir := os.MkdirAll(userDirectory, 0751)
605 | if errDir != nil {
606 | log.Error().Err(errDir).Msg("Could not create user directory")
607 | return
608 | }
609 | }
610 |
611 | id := atomic.AddInt32(&historySyncID, 1)
612 | fileName := filepath.Join(userDirectory, "history-"+strconv.Itoa(int(id))+".json")
613 | file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
614 | if err != nil {
615 | log.Error().Err(err).Msg("Failed to open file to write history sync")
616 | return
617 | }
618 | enc := json.NewEncoder(file)
619 | enc.SetIndent("", " ")
620 | err = enc.Encode(evt.Data)
621 | if err != nil {
622 | log.Error().Err(err).Msg("Failed to write history sync")
623 | return
624 | }
625 | log.Info().Str("filename", fileName).Msg("Wrote history sync")
626 | _ = file.Close()
627 | case *events.AppState:
628 | log.Info().Str("index", fmt.Sprintf("%+v", evt.Index)).Str("actionValue", fmt.Sprintf("%+v", evt.SyncActionValue)).Msg("App state event received")
629 | case *events.LoggedOut:
630 | log.Info().Str("reason", evt.Reason.String()).Msg("Logged out")
631 | killchannel[mycli.userID] <- true
632 | sqlStmt := `UPDATE users SET connected=0 WHERE id=$1`
633 | _, err := mycli.db.Exec(sqlStmt, mycli.userID)
634 | if err != nil {
635 | log.Error().Err(err).Msg(sqlStmt)
636 | return
637 | }
638 | case *events.ChatPresence:
639 | postmap["type"] = "ChatPresence"
640 | dowebhook = 1
641 | log.Info().Str("state", fmt.Sprintf("%s", evt.State)).Str("media", fmt.Sprintf("%s", evt.Media)).Str("chat", evt.MessageSource.Chat.String()).Str("sender", evt.MessageSource.Sender.String()).Msg("Chat Presence received")
642 | case *events.CallOffer:
643 | log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call offer")
644 | case *events.CallAccept:
645 | log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call accept")
646 | case *events.CallTerminate:
647 | log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call terminate")
648 | case *events.CallOfferNotice:
649 | log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call offer notice")
650 | case *events.CallRelayLatency:
651 | log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call relay latency")
652 | default:
653 | log.Warn().Str("event", fmt.Sprintf("%+v", evt)).Msg("Unhandled event")
654 | }
655 |
656 | if dowebhook == 1 {
657 | // call webhook
658 | webhookurl := ""
659 | myuserinfo, found := userinfocache.Get(mycli.token)
660 | if !found {
661 | log.Warn().Str("token", mycli.token).Msg("Could not call webhook as there is no user for this token")
662 | } else {
663 | webhookurl = myuserinfo.(Values).Get("Webhook")
664 | }
665 |
666 | if !Find(mycli.subscriptions, postmap["type"].(string)) && !Find(mycli.subscriptions, "All") {
667 | log.Warn().Str("type", postmap["type"].(string)).Msg("Skipping webhook. Not subscribed for this type")
668 | return
669 | }
670 |
671 | if webhookurl != "" {
672 | log.Info().Str("url", webhookurl).Msg("Calling webhook")
673 | filteredPostmap := filterBase64Data(postmap)
674 | jsonData, err := json.Marshal(filteredPostmap)
675 | // jsonData, err := json.Marshal(postmap)
676 | if err != nil {
677 | log.Error().Err(err).Msg("Failed to marshal postmap to JSON")
678 | } else {
679 | data := map[string]string{
680 | "jsonData": string(jsonData),
681 | "token": mycli.token,
682 | }
683 |
684 | // Adicione este log
685 | log.Debug().Interface("webhookData", data).Msg("Data being sent to webhook")
686 |
687 | if path == "" {
688 | go callHook(webhookurl, data, mycli.userID)
689 | } else {
690 | // Create a channel to capture error from the goroutine
691 | errChan := make(chan error, 1)
692 | go func() {
693 | // err := callHookFile(webhookurl, data, mycli.userID, path)
694 | err := callHookFile(strconv.Itoa(mycli.userID), data, filepath.Base(path), webhookurl)
695 | errChan <- err
696 | }()
697 |
698 | // Optionally handle the error from the channel
699 | if err := <-errChan; err != nil {
700 | log.Error().Err(err).Msg("Error calling hook file")
701 | }
702 | }
703 | }
704 | } else {
705 | log.Warn().Str("userid", strconv.Itoa(mycli.userID)).Msg("No webhook set for user")
706 | }
707 | }
708 | }
709 |
--------------------------------------------------------------------------------
/wuzapi.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pedroherpeto/wuzapi/e1d689e33dcdf9b6d68fd5d6d074fe39fc4d0ab3/wuzapi.exe
--------------------------------------------------------------------------------
/wuzapi.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Wuzapi
3 | After=network-online.target
4 | Wants=network-online.target systemd-networkd-wait-online.service
5 |
6 | StartLimitIntervalSec=500
7 | StartLimitBurst=5
8 |
9 | [Service]
10 | Restart=on-failure
11 | RestartSec=5s
12 |
13 | ExecStart=/usr/local/wuzapi/wuzapi -wadebug DEBUG
14 |
15 | [Install]
16 | WantedBy=multi-user.target
17 |
--------------------------------------------------------------------------------