Spaces:
Runtime error
Runtime error
Merge pull request #20 from orion-ai-project/dev
Browse filesfeat: adding whatsapp/socket for websocket connection
- controllers/socket_controller.go +41 -0
- docs/docs.go +84 -2
- docs/swagger.json +84 -2
- docs/swagger.yaml +54 -2
- go.mod +2 -1
- go.sum +2 -0
- middleware/auth_middleware.go +7 -7
- provider/controller_provider.go +8 -0
- provider/services_provider.go +9 -1
- router/router.go +1 -0
- router/socket_router.go +19 -0
- services/connection_service.go +8 -2
- services/socket_service.go +130 -0
controllers/socket_controller.go
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
http_error "whatsapp-backend/models/error"
|
| 6 |
+
"whatsapp-backend/services"
|
| 7 |
+
"whatsapp-backend/utils"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type SocketController interface {
|
| 14 |
+
HandleWebSocket(ctx *gin.Context)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type socketController struct {
|
| 18 |
+
socketService services.SocketService
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func NewSocketController(socketService services.SocketService) SocketController {
|
| 22 |
+
return &socketController{socketService: socketService}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// HandleWebSocket godoc
|
| 26 |
+
// @Summary Connect to WebSocket
|
| 27 |
+
// @Description Upgrade HTTP connection to WebSocket for real-time updates
|
| 28 |
+
// @Tags whatsapp
|
| 29 |
+
// @Security BearerAuth
|
| 30 |
+
// @Success 101
|
| 31 |
+
// @Router /whatsapp/socket [get]
|
| 32 |
+
func (c *socketController) HandleWebSocket(ctx *gin.Context) {
|
| 33 |
+
userID, exists := ctx.Get("user_id")
|
| 34 |
+
if !exists {
|
| 35 |
+
log.Println("[WebSocket] User ID not found in context")
|
| 36 |
+
utils.SendResponse[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
accountID := userID.(uuid.UUID)
|
| 40 |
+
c.socketService.HandleConnection(ctx.Writer, ctx.Request, accountID)
|
| 41 |
+
}
|
docs/docs.go
CHANGED
|
@@ -443,7 +443,70 @@ const docTemplate = `{
|
|
| 443 |
}
|
| 444 |
}
|
| 445 |
},
|
| 446 |
-
"/chat/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
"post": {
|
| 448 |
"security": [
|
| 449 |
{
|
|
@@ -460,7 +523,7 @@ const docTemplate = `{
|
|
| 460 |
"tags": [
|
| 461 |
"chat"
|
| 462 |
],
|
| 463 |
-
"summary": "Send WhatsApp message",
|
| 464 |
"parameters": [
|
| 465 |
{
|
| 466 |
"description": "Send Message Request",
|
|
@@ -545,6 +608,25 @@ const docTemplate = `{
|
|
| 545 |
}
|
| 546 |
}
|
| 547 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
"/whatsapp/status": {
|
| 549 |
"get": {
|
| 550 |
"security": [
|
|
|
|
| 443 |
}
|
| 444 |
}
|
| 445 |
},
|
| 446 |
+
"/chat/send_img": {
|
| 447 |
+
"post": {
|
| 448 |
+
"security": [
|
| 449 |
+
{
|
| 450 |
+
"BearerAuth": []
|
| 451 |
+
}
|
| 452 |
+
],
|
| 453 |
+
"description": "Send an image to a WhatsApp number via file upload",
|
| 454 |
+
"consumes": [
|
| 455 |
+
"multipart/form-data"
|
| 456 |
+
],
|
| 457 |
+
"produces": [
|
| 458 |
+
"application/json"
|
| 459 |
+
],
|
| 460 |
+
"tags": [
|
| 461 |
+
"chat"
|
| 462 |
+
],
|
| 463 |
+
"summary": "Send WhatsApp image",
|
| 464 |
+
"parameters": [
|
| 465 |
+
{
|
| 466 |
+
"type": "file",
|
| 467 |
+
"description": "Image file to send",
|
| 468 |
+
"name": "image",
|
| 469 |
+
"in": "formData",
|
| 470 |
+
"required": true
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"type": "string",
|
| 474 |
+
"example": "628123456789",
|
| 475 |
+
"description": "Recipient phone number",
|
| 476 |
+
"name": "recipient",
|
| 477 |
+
"in": "formData",
|
| 478 |
+
"required": true
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"type": "string",
|
| 482 |
+
"description": "Image caption",
|
| 483 |
+
"name": "caption",
|
| 484 |
+
"in": "formData"
|
| 485 |
+
}
|
| 486 |
+
],
|
| 487 |
+
"responses": {
|
| 488 |
+
"200": {
|
| 489 |
+
"description": "OK",
|
| 490 |
+
"schema": {
|
| 491 |
+
"$ref": "#/definitions/dto.SendMessageResponse"
|
| 492 |
+
}
|
| 493 |
+
},
|
| 494 |
+
"400": {
|
| 495 |
+
"description": "Bad Request",
|
| 496 |
+
"schema": {
|
| 497 |
+
"$ref": "#/definitions/dto.ErrorResponse"
|
| 498 |
+
}
|
| 499 |
+
},
|
| 500 |
+
"401": {
|
| 501 |
+
"description": "Unauthorized",
|
| 502 |
+
"schema": {
|
| 503 |
+
"$ref": "#/definitions/dto.ErrorResponse"
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
},
|
| 509 |
+
"/chat/send_msg": {
|
| 510 |
"post": {
|
| 511 |
"security": [
|
| 512 |
{
|
|
|
|
| 523 |
"tags": [
|
| 524 |
"chat"
|
| 525 |
],
|
| 526 |
+
"summary": "Send WhatsApp text message",
|
| 527 |
"parameters": [
|
| 528 |
{
|
| 529 |
"description": "Send Message Request",
|
|
|
|
| 608 |
}
|
| 609 |
}
|
| 610 |
},
|
| 611 |
+
"/whatsapp/socket": {
|
| 612 |
+
"get": {
|
| 613 |
+
"security": [
|
| 614 |
+
{
|
| 615 |
+
"BearerAuth": []
|
| 616 |
+
}
|
| 617 |
+
],
|
| 618 |
+
"description": "Upgrade HTTP connection to WebSocket for real-time updates",
|
| 619 |
+
"tags": [
|
| 620 |
+
"whatsapp"
|
| 621 |
+
],
|
| 622 |
+
"summary": "Connect to WebSocket",
|
| 623 |
+
"responses": {
|
| 624 |
+
"101": {
|
| 625 |
+
"description": "Switching Protocols"
|
| 626 |
+
}
|
| 627 |
+
}
|
| 628 |
+
}
|
| 629 |
+
},
|
| 630 |
"/whatsapp/status": {
|
| 631 |
"get": {
|
| 632 |
"security": [
|
docs/swagger.json
CHANGED
|
@@ -436,7 +436,70 @@
|
|
| 436 |
}
|
| 437 |
}
|
| 438 |
},
|
| 439 |
-
"/chat/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
"post": {
|
| 441 |
"security": [
|
| 442 |
{
|
|
@@ -453,7 +516,7 @@
|
|
| 453 |
"tags": [
|
| 454 |
"chat"
|
| 455 |
],
|
| 456 |
-
"summary": "Send WhatsApp message",
|
| 457 |
"parameters": [
|
| 458 |
{
|
| 459 |
"description": "Send Message Request",
|
|
@@ -538,6 +601,25 @@
|
|
| 538 |
}
|
| 539 |
}
|
| 540 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
"/whatsapp/status": {
|
| 542 |
"get": {
|
| 543 |
"security": [
|
|
|
|
| 436 |
}
|
| 437 |
}
|
| 438 |
},
|
| 439 |
+
"/chat/send_img": {
|
| 440 |
+
"post": {
|
| 441 |
+
"security": [
|
| 442 |
+
{
|
| 443 |
+
"BearerAuth": []
|
| 444 |
+
}
|
| 445 |
+
],
|
| 446 |
+
"description": "Send an image to a WhatsApp number via file upload",
|
| 447 |
+
"consumes": [
|
| 448 |
+
"multipart/form-data"
|
| 449 |
+
],
|
| 450 |
+
"produces": [
|
| 451 |
+
"application/json"
|
| 452 |
+
],
|
| 453 |
+
"tags": [
|
| 454 |
+
"chat"
|
| 455 |
+
],
|
| 456 |
+
"summary": "Send WhatsApp image",
|
| 457 |
+
"parameters": [
|
| 458 |
+
{
|
| 459 |
+
"type": "file",
|
| 460 |
+
"description": "Image file to send",
|
| 461 |
+
"name": "image",
|
| 462 |
+
"in": "formData",
|
| 463 |
+
"required": true
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"type": "string",
|
| 467 |
+
"example": "628123456789",
|
| 468 |
+
"description": "Recipient phone number",
|
| 469 |
+
"name": "recipient",
|
| 470 |
+
"in": "formData",
|
| 471 |
+
"required": true
|
| 472 |
+
},
|
| 473 |
+
{
|
| 474 |
+
"type": "string",
|
| 475 |
+
"description": "Image caption",
|
| 476 |
+
"name": "caption",
|
| 477 |
+
"in": "formData"
|
| 478 |
+
}
|
| 479 |
+
],
|
| 480 |
+
"responses": {
|
| 481 |
+
"200": {
|
| 482 |
+
"description": "OK",
|
| 483 |
+
"schema": {
|
| 484 |
+
"$ref": "#/definitions/dto.SendMessageResponse"
|
| 485 |
+
}
|
| 486 |
+
},
|
| 487 |
+
"400": {
|
| 488 |
+
"description": "Bad Request",
|
| 489 |
+
"schema": {
|
| 490 |
+
"$ref": "#/definitions/dto.ErrorResponse"
|
| 491 |
+
}
|
| 492 |
+
},
|
| 493 |
+
"401": {
|
| 494 |
+
"description": "Unauthorized",
|
| 495 |
+
"schema": {
|
| 496 |
+
"$ref": "#/definitions/dto.ErrorResponse"
|
| 497 |
+
}
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
},
|
| 502 |
+
"/chat/send_msg": {
|
| 503 |
"post": {
|
| 504 |
"security": [
|
| 505 |
{
|
|
|
|
| 516 |
"tags": [
|
| 517 |
"chat"
|
| 518 |
],
|
| 519 |
+
"summary": "Send WhatsApp text message",
|
| 520 |
"parameters": [
|
| 521 |
{
|
| 522 |
"description": "Send Message Request",
|
|
|
|
| 601 |
}
|
| 602 |
}
|
| 603 |
},
|
| 604 |
+
"/whatsapp/socket": {
|
| 605 |
+
"get": {
|
| 606 |
+
"security": [
|
| 607 |
+
{
|
| 608 |
+
"BearerAuth": []
|
| 609 |
+
}
|
| 610 |
+
],
|
| 611 |
+
"description": "Upgrade HTTP connection to WebSocket for real-time updates",
|
| 612 |
+
"tags": [
|
| 613 |
+
"whatsapp"
|
| 614 |
+
],
|
| 615 |
+
"summary": "Connect to WebSocket",
|
| 616 |
+
"responses": {
|
| 617 |
+
"101": {
|
| 618 |
+
"description": "Switching Protocols"
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
}
|
| 622 |
+
},
|
| 623 |
"/whatsapp/status": {
|
| 624 |
"get": {
|
| 625 |
"security": [
|
docs/swagger.yaml
CHANGED
|
@@ -453,7 +453,48 @@ paths:
|
|
| 453 |
summary: Fetch WhatsApp contacts
|
| 454 |
tags:
|
| 455 |
- chat
|
| 456 |
-
/chat/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
post:
|
| 458 |
consumes:
|
| 459 |
- application/json
|
|
@@ -482,7 +523,7 @@ paths:
|
|
| 482 |
$ref: '#/definitions/dto.ErrorResponse'
|
| 483 |
security:
|
| 484 |
- BearerAuth: []
|
| 485 |
-
summary: Send WhatsApp message
|
| 486 |
tags:
|
| 487 |
- chat
|
| 488 |
/whatsapp/connect:
|
|
@@ -518,6 +559,17 @@ paths:
|
|
| 518 |
summary: Connect new WhatsApp account
|
| 519 |
tags:
|
| 520 |
- whatsapp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
/whatsapp/status:
|
| 522 |
get:
|
| 523 |
consumes:
|
|
|
|
| 453 |
summary: Fetch WhatsApp contacts
|
| 454 |
tags:
|
| 455 |
- chat
|
| 456 |
+
/chat/send_img:
|
| 457 |
+
post:
|
| 458 |
+
consumes:
|
| 459 |
+
- multipart/form-data
|
| 460 |
+
description: Send an image to a WhatsApp number via file upload
|
| 461 |
+
parameters:
|
| 462 |
+
- description: Image file to send
|
| 463 |
+
in: formData
|
| 464 |
+
name: image
|
| 465 |
+
required: true
|
| 466 |
+
type: file
|
| 467 |
+
- description: Recipient phone number
|
| 468 |
+
example: "628123456789"
|
| 469 |
+
in: formData
|
| 470 |
+
name: recipient
|
| 471 |
+
required: true
|
| 472 |
+
type: string
|
| 473 |
+
- description: Image caption
|
| 474 |
+
in: formData
|
| 475 |
+
name: caption
|
| 476 |
+
type: string
|
| 477 |
+
produces:
|
| 478 |
+
- application/json
|
| 479 |
+
responses:
|
| 480 |
+
"200":
|
| 481 |
+
description: OK
|
| 482 |
+
schema:
|
| 483 |
+
$ref: '#/definitions/dto.SendMessageResponse'
|
| 484 |
+
"400":
|
| 485 |
+
description: Bad Request
|
| 486 |
+
schema:
|
| 487 |
+
$ref: '#/definitions/dto.ErrorResponse'
|
| 488 |
+
"401":
|
| 489 |
+
description: Unauthorized
|
| 490 |
+
schema:
|
| 491 |
+
$ref: '#/definitions/dto.ErrorResponse'
|
| 492 |
+
security:
|
| 493 |
+
- BearerAuth: []
|
| 494 |
+
summary: Send WhatsApp image
|
| 495 |
+
tags:
|
| 496 |
+
- chat
|
| 497 |
+
/chat/send_msg:
|
| 498 |
post:
|
| 499 |
consumes:
|
| 500 |
- application/json
|
|
|
|
| 523 |
$ref: '#/definitions/dto.ErrorResponse'
|
| 524 |
security:
|
| 525 |
- BearerAuth: []
|
| 526 |
+
summary: Send WhatsApp text message
|
| 527 |
tags:
|
| 528 |
- chat
|
| 529 |
/whatsapp/connect:
|
|
|
|
| 559 |
summary: Connect new WhatsApp account
|
| 560 |
tags:
|
| 561 |
- whatsapp
|
| 562 |
+
/whatsapp/socket:
|
| 563 |
+
get:
|
| 564 |
+
description: Upgrade HTTP connection to WebSocket for real-time updates
|
| 565 |
+
responses:
|
| 566 |
+
"101":
|
| 567 |
+
description: Switching Protocols
|
| 568 |
+
security:
|
| 569 |
+
- BearerAuth: []
|
| 570 |
+
summary: Connect to WebSocket
|
| 571 |
+
tags:
|
| 572 |
+
- whatsapp
|
| 573 |
/whatsapp/status:
|
| 574 |
get:
|
| 575 |
consumes:
|
go.mod
CHANGED
|
@@ -8,12 +8,14 @@ require (
|
|
| 8 |
github.com/go-playground/validator/v10 v10.29.0
|
| 9 |
github.com/golang-jwt/jwt/v5 v5.3.0
|
| 10 |
github.com/google/uuid v1.6.0
|
|
|
|
| 11 |
github.com/joho/godotenv v1.5.1
|
| 12 |
github.com/swaggo/files v1.0.1
|
| 13 |
github.com/swaggo/gin-swagger v1.6.1
|
| 14 |
github.com/swaggo/swag v1.16.6
|
| 15 |
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
|
| 16 |
golang.org/x/crypto v0.46.0
|
|
|
|
| 17 |
gorm.io/driver/postgres v1.6.0
|
| 18 |
gorm.io/gorm v1.31.1
|
| 19 |
)
|
|
@@ -77,5 +79,4 @@ require (
|
|
| 77 |
golang.org/x/sys v0.39.0 // indirect
|
| 78 |
golang.org/x/text v0.32.0 // indirect
|
| 79 |
golang.org/x/tools v0.40.0 // indirect
|
| 80 |
-
google.golang.org/protobuf v1.36.11 // indirect
|
| 81 |
)
|
|
|
|
| 8 |
github.com/go-playground/validator/v10 v10.29.0
|
| 9 |
github.com/golang-jwt/jwt/v5 v5.3.0
|
| 10 |
github.com/google/uuid v1.6.0
|
| 11 |
+
github.com/gorilla/websocket v1.5.3
|
| 12 |
github.com/joho/godotenv v1.5.1
|
| 13 |
github.com/swaggo/files v1.0.1
|
| 14 |
github.com/swaggo/gin-swagger v1.6.1
|
| 15 |
github.com/swaggo/swag v1.16.6
|
| 16 |
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
|
| 17 |
golang.org/x/crypto v0.46.0
|
| 18 |
+
google.golang.org/protobuf v1.36.11
|
| 19 |
gorm.io/driver/postgres v1.6.0
|
| 20 |
gorm.io/gorm v1.31.1
|
| 21 |
)
|
|
|
|
| 79 |
golang.org/x/sys v0.39.0 // indirect
|
| 80 |
golang.org/x/text v0.32.0 // indirect
|
| 81 |
golang.org/x/tools v0.40.0 // indirect
|
|
|
|
| 82 |
)
|
go.sum
CHANGED
|
@@ -82,6 +82,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|
| 82 |
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 83 |
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 84 |
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
|
|
|
|
|
| 85 |
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
| 86 |
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
| 87 |
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
|
|
| 82 |
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 83 |
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 84 |
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 85 |
+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
| 86 |
+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
| 87 |
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
| 88 |
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
| 89 |
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
middleware/auth_middleware.go
CHANGED
|
@@ -29,14 +29,14 @@ func NewAuthMiddleware(jwtConfig config.JWTConfig, authRepo repositories.AuthRep
|
|
| 29 |
func (m *authMiddleware) RequireAuth() gin.HandlerFunc {
|
| 30 |
return func(ctx *gin.Context) {
|
| 31 |
authHeader := ctx.GetHeader("Authorization")
|
| 32 |
-
|
| 33 |
-
utils.SendResponse[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 34 |
-
ctx.Abort()
|
| 35 |
-
return
|
| 36 |
-
}
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
if tokenString == "" {
|
| 42 |
utils.SendResponse[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
|
|
|
| 29 |
func (m *authMiddleware) RequireAuth() gin.HandlerFunc {
|
| 30 |
return func(ctx *gin.Context) {
|
| 31 |
authHeader := ctx.GetHeader("Authorization")
|
| 32 |
+
tokenString := ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
if authHeader != "" {
|
| 35 |
+
tokenString = strings.TrimPrefix(authHeader, "Bearer")
|
| 36 |
+
tokenString = strings.TrimSpace(tokenString)
|
| 37 |
+
} else {
|
| 38 |
+
tokenString = ctx.Query("token")
|
| 39 |
+
}
|
| 40 |
|
| 41 |
if tokenString == "" {
|
| 42 |
utils.SendResponse[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
provider/controller_provider.go
CHANGED
|
@@ -7,6 +7,7 @@ type ControllerProvider interface {
|
|
| 7 |
ProvideAuthController() controllers.AuthController
|
| 8 |
ProvideChatController() controllers.ChatController
|
| 9 |
ProvideAdminController() controllers.AdminController
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
type controllerProvider struct {
|
|
@@ -14,6 +15,7 @@ type controllerProvider struct {
|
|
| 14 |
authController controllers.AuthController
|
| 15 |
chatController controllers.ChatController
|
| 16 |
adminController controllers.AdminController
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
|
|
@@ -22,12 +24,14 @@ func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider
|
|
| 22 |
authController := controllers.NewAuthController(servicesProvider.ProvideAuthService())
|
| 23 |
chatController := controllers.NewChatController(servicesProvider.ProvideChatService())
|
| 24 |
adminController := controllers.NewAdminController(servicesProvider.ProvideAdminService())
|
|
|
|
| 25 |
|
| 26 |
return &controllerProvider{
|
| 27 |
connectionController: connectionController,
|
| 28 |
authController: authController,
|
| 29 |
chatController: chatController,
|
| 30 |
adminController: adminController,
|
|
|
|
| 31 |
}
|
| 32 |
}
|
| 33 |
|
|
@@ -46,3 +50,7 @@ func (c *controllerProvider) ProvideChatController() controllers.ChatController
|
|
| 46 |
func (c *controllerProvider) ProvideAdminController() controllers.AdminController {
|
| 47 |
return c.adminController
|
| 48 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
ProvideAuthController() controllers.AuthController
|
| 8 |
ProvideChatController() controllers.ChatController
|
| 9 |
ProvideAdminController() controllers.AdminController
|
| 10 |
+
ProvideSocketController() controllers.SocketController
|
| 11 |
}
|
| 12 |
|
| 13 |
type controllerProvider struct {
|
|
|
|
| 15 |
authController controllers.AuthController
|
| 16 |
chatController controllers.ChatController
|
| 17 |
adminController controllers.AdminController
|
| 18 |
+
socketController controllers.SocketController
|
| 19 |
}
|
| 20 |
|
| 21 |
func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
|
|
|
|
| 24 |
authController := controllers.NewAuthController(servicesProvider.ProvideAuthService())
|
| 25 |
chatController := controllers.NewChatController(servicesProvider.ProvideChatService())
|
| 26 |
adminController := controllers.NewAdminController(servicesProvider.ProvideAdminService())
|
| 27 |
+
socketController := controllers.NewSocketController(servicesProvider.ProvideSocketService())
|
| 28 |
|
| 29 |
return &controllerProvider{
|
| 30 |
connectionController: connectionController,
|
| 31 |
authController: authController,
|
| 32 |
chatController: chatController,
|
| 33 |
adminController: adminController,
|
| 34 |
+
socketController: socketController,
|
| 35 |
}
|
| 36 |
}
|
| 37 |
|
|
|
|
| 50 |
func (c *controllerProvider) ProvideAdminController() controllers.AdminController {
|
| 51 |
return c.adminController
|
| 52 |
}
|
| 53 |
+
|
| 54 |
+
func (c *controllerProvider) ProvideSocketController() controllers.SocketController {
|
| 55 |
+
return c.socketController
|
| 56 |
+
}
|
provider/services_provider.go
CHANGED
|
@@ -7,6 +7,7 @@ type ServicesProvider interface {
|
|
| 7 |
ProvideAuthService() services.AuthService
|
| 8 |
ProvideChatService() services.ChatService
|
| 9 |
ProvideAdminService() services.AdminService
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
type servicesProvider struct {
|
|
@@ -14,10 +15,12 @@ type servicesProvider struct {
|
|
| 14 |
authService services.AuthService
|
| 15 |
chatService services.ChatService
|
| 16 |
adminService services.AdminService
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {
|
| 20 |
-
|
|
|
|
| 21 |
authService := services.NewAuthService(repoProvider.ProvideAuthRepository(), repoProvider.ProvideConnectionRepository(), configProvider.ProvideJWTConfig(), configProvider.ProvideEnvConfig())
|
| 22 |
chatService := services.NewChatService(connectionService, repoProvider.ProvideChatRepository())
|
| 23 |
adminService := services.NewAdminService(repoProvider.ProvideAuthRepository(), configProvider.ProvideEnvConfig())
|
|
@@ -27,6 +30,7 @@ func NewServicesProvider(repoProvider RepositoriesProvider, configProvider Confi
|
|
| 27 |
authService: authService,
|
| 28 |
chatService: chatService,
|
| 29 |
adminService: adminService,
|
|
|
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
|
@@ -45,3 +49,7 @@ func (s *servicesProvider) ProvideChatService() services.ChatService {
|
|
| 45 |
func (s *servicesProvider) ProvideAdminService() services.AdminService {
|
| 46 |
return s.adminService
|
| 47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
ProvideAuthService() services.AuthService
|
| 8 |
ProvideChatService() services.ChatService
|
| 9 |
ProvideAdminService() services.AdminService
|
| 10 |
+
ProvideSocketService() services.SocketService
|
| 11 |
}
|
| 12 |
|
| 13 |
type servicesProvider struct {
|
|
|
|
| 15 |
authService services.AuthService
|
| 16 |
chatService services.ChatService
|
| 17 |
adminService services.AdminService
|
| 18 |
+
socketService services.SocketService
|
| 19 |
}
|
| 20 |
|
| 21 |
func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {
|
| 22 |
+
socketService := services.NewSocketService()
|
| 23 |
+
connectionService := services.NewConnectionService(repoProvider.ProvideConnectionRepository(), socketService)
|
| 24 |
authService := services.NewAuthService(repoProvider.ProvideAuthRepository(), repoProvider.ProvideConnectionRepository(), configProvider.ProvideJWTConfig(), configProvider.ProvideEnvConfig())
|
| 25 |
chatService := services.NewChatService(connectionService, repoProvider.ProvideChatRepository())
|
| 26 |
adminService := services.NewAdminService(repoProvider.ProvideAuthRepository(), configProvider.ProvideEnvConfig())
|
|
|
|
| 30 |
authService: authService,
|
| 31 |
chatService: chatService,
|
| 32 |
adminService: adminService,
|
| 33 |
+
socketService: socketService,
|
| 34 |
}
|
| 35 |
}
|
| 36 |
|
|
|
|
| 49 |
func (s *servicesProvider) ProvideAdminService() services.AdminService {
|
| 50 |
return s.adminService
|
| 51 |
}
|
| 52 |
+
|
| 53 |
+
func (s *servicesProvider) ProvideSocketService() services.SocketService {
|
| 54 |
+
return s.socketService
|
| 55 |
+
}
|
router/router.go
CHANGED
|
@@ -16,6 +16,7 @@ func RunRouter(appProvider provider.AppProvider) {
|
|
| 16 |
AuthRouter(router, controller, middleware)
|
| 17 |
ChatRouter(router, controller, middleware)
|
| 18 |
AdminRouter(router, controller, middleware)
|
|
|
|
| 19 |
|
| 20 |
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
| 21 |
|
|
|
|
| 16 |
AuthRouter(router, controller, middleware)
|
| 17 |
ChatRouter(router, controller, middleware)
|
| 18 |
AdminRouter(router, controller, middleware)
|
| 19 |
+
SocketRouter(router, controller, middleware)
|
| 20 |
|
| 21 |
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
| 22 |
|
router/socket_router.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"whatsapp-backend/provider"
|
| 5 |
+
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func SocketRouter(router *gin.Engine, controller provider.ControllerProvider, middleware provider.MiddlewareProvider) {
|
| 10 |
+
socketController := controller.ProvideSocketController()
|
| 11 |
+
authMiddleware := middleware.ProvideAuthMiddleware()
|
| 12 |
+
|
| 13 |
+
// WebSocket endpoint needs to be authenticated
|
| 14 |
+
routerGroup := router.Group("/api/whatsapp", authMiddleware.RequireAuth())
|
| 15 |
+
// routerGroup.Use(gzip.Gzip(gzip.DefaultCompression)) // gzip might interfere with websocket upgrade
|
| 16 |
+
{
|
| 17 |
+
routerGroup.GET("/socket", socketController.HandleWebSocket)
|
| 18 |
+
}
|
| 19 |
+
}
|
services/connection_service.go
CHANGED
|
@@ -20,12 +20,14 @@ type ConnectionService interface {
|
|
| 20 |
|
| 21 |
type connectionService struct {
|
| 22 |
connectionRepo repositories.ConnectionRepository
|
|
|
|
| 23 |
activeClients sync.Map
|
| 24 |
}
|
| 25 |
|
| 26 |
-
func NewConnectionService(connectionRepo repositories.ConnectionRepository) ConnectionService {
|
| 27 |
return &connectionService{
|
| 28 |
connectionRepo: connectionRepo,
|
|
|
|
| 29 |
}
|
| 30 |
}
|
| 31 |
|
|
@@ -52,6 +54,7 @@ func (s *connectionService) Connect(ctx context.Context, accountID uuid.UUID) (s
|
|
| 52 |
go func() {
|
| 53 |
for evt := range qrChan {
|
| 54 |
if evt.Event == "code" {
|
|
|
|
| 55 |
select {
|
| 56 |
case qrCodeChan <- evt.Code:
|
| 57 |
default:
|
|
@@ -95,19 +98,22 @@ func (s *connectionService) handleWhatsAppEvent(accountID uuid.UUID, client *wha
|
|
| 95 |
jid := client.Store.ID.String()
|
| 96 |
if s.connectionRepo.IsJIDUsedByOther(jid, accountID) {
|
| 97 |
log.Printf("[WhatsApp] JID conflict detected for account %s", accountID)
|
|
|
|
| 98 |
client.Disconnect()
|
| 99 |
s.activeClients.Delete(accountID)
|
| 100 |
return
|
| 101 |
}
|
| 102 |
|
| 103 |
_ = s.connectionRepo.UpdateAccountStatus(accountID, jid, true)
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
case *events.LoggedOut:
|
| 107 |
s.activeClients.Delete(accountID)
|
| 108 |
_ = s.connectionRepo.UpdateAccountStatus(accountID, "", false)
|
|
|
|
| 109 |
|
| 110 |
case *events.Disconnected:
|
| 111 |
-
|
| 112 |
}
|
| 113 |
}
|
|
|
|
| 20 |
|
| 21 |
type connectionService struct {
|
| 22 |
connectionRepo repositories.ConnectionRepository
|
| 23 |
+
socketService SocketService
|
| 24 |
activeClients sync.Map
|
| 25 |
}
|
| 26 |
|
| 27 |
+
func NewConnectionService(connectionRepo repositories.ConnectionRepository, socketService SocketService) ConnectionService {
|
| 28 |
return &connectionService{
|
| 29 |
connectionRepo: connectionRepo,
|
| 30 |
+
socketService: socketService,
|
| 31 |
}
|
| 32 |
}
|
| 33 |
|
|
|
|
| 54 |
go func() {
|
| 55 |
for evt := range qrChan {
|
| 56 |
if evt.Event == "code" {
|
| 57 |
+
s.socketService.EmitEvent(accountID, "qr_code", evt.Code)
|
| 58 |
select {
|
| 59 |
case qrCodeChan <- evt.Code:
|
| 60 |
default:
|
|
|
|
| 98 |
jid := client.Store.ID.String()
|
| 99 |
if s.connectionRepo.IsJIDUsedByOther(jid, accountID) {
|
| 100 |
log.Printf("[WhatsApp] JID conflict detected for account %s", accountID)
|
| 101 |
+
s.socketService.EmitEvent(accountID, "error", "JID conflict: This number is already connected to another account")
|
| 102 |
client.Disconnect()
|
| 103 |
s.activeClients.Delete(accountID)
|
| 104 |
return
|
| 105 |
}
|
| 106 |
|
| 107 |
_ = s.connectionRepo.UpdateAccountStatus(accountID, jid, true)
|
| 108 |
+
s.socketService.EmitEvent(accountID, "connected", map[string]string{"jid": jid})
|
| 109 |
}
|
| 110 |
|
| 111 |
case *events.LoggedOut:
|
| 112 |
s.activeClients.Delete(accountID)
|
| 113 |
_ = s.connectionRepo.UpdateAccountStatus(accountID, "", false)
|
| 114 |
+
s.socketService.EmitEvent(accountID, "logged_out", nil)
|
| 115 |
|
| 116 |
case *events.Disconnected:
|
| 117 |
+
s.socketService.EmitEvent(accountID, "disconnected", nil)
|
| 118 |
}
|
| 119 |
}
|
services/socket_service.go
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"net/http"
|
| 6 |
+
"sync"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
"github.com/gorilla/websocket"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type SocketService interface {
|
| 14 |
+
HandleConnection(w http.ResponseWriter, r *http.Request, userID uuid.UUID)
|
| 15 |
+
EmitEvent(userID uuid.UUID, eventType string, payload interface{})
|
| 16 |
+
BroadcastEvent(eventType string, payload interface{})
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type socketService struct {
|
| 20 |
+
clients map[uuid.UUID][]*websocket.Conn
|
| 21 |
+
clientsMux sync.RWMutex
|
| 22 |
+
upgrader websocket.Upgrader
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func NewSocketService() SocketService {
|
| 26 |
+
return &socketService{
|
| 27 |
+
clients: make(map[uuid.UUID][]*websocket.Conn),
|
| 28 |
+
upgrader: websocket.Upgrader{
|
| 29 |
+
ReadBufferSize: 1024,
|
| 30 |
+
WriteBufferSize: 1024,
|
| 31 |
+
CheckOrigin: func(r *http.Request) bool {
|
| 32 |
+
return true // Development mode
|
| 33 |
+
},
|
| 34 |
+
},
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func (s *socketService) HandleConnection(w http.ResponseWriter, r *http.Request, userID uuid.UUID) {
|
| 39 |
+
conn, err := s.upgrader.Upgrade(w, r, nil)
|
| 40 |
+
if err != nil {
|
| 41 |
+
log.Printf("Failed to upgrade connection: %v", err)
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
s.registerClient(userID, conn)
|
| 46 |
+
defer s.unregisterClient(userID, conn)
|
| 47 |
+
|
| 48 |
+
// Keep connection alive and listen for disconnects
|
| 49 |
+
for {
|
| 50 |
+
_, _, err := conn.ReadMessage()
|
| 51 |
+
if err != nil {
|
| 52 |
+
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
| 53 |
+
log.Printf("error: %v", err)
|
| 54 |
+
}
|
| 55 |
+
break
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
func (s *socketService) registerClient(userID uuid.UUID, conn *websocket.Conn) {
|
| 61 |
+
s.clientsMux.Lock()
|
| 62 |
+
defer s.clientsMux.Unlock()
|
| 63 |
+
|
| 64 |
+
s.clients[userID] = append(s.clients[userID], conn)
|
| 65 |
+
log.Printf("Client connected: %s", userID)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
func (s *socketService) unregisterClient(userID uuid.UUID, conn *websocket.Conn) {
|
| 69 |
+
s.clientsMux.Lock()
|
| 70 |
+
defer s.clientsMux.Unlock()
|
| 71 |
+
|
| 72 |
+
conn.Close()
|
| 73 |
+
|
| 74 |
+
if conns, ok := s.clients[userID]; ok {
|
| 75 |
+
for i, c := range conns {
|
| 76 |
+
if c == conn {
|
| 77 |
+
s.clients[userID] = append(conns[:i], conns[i+1:]...)
|
| 78 |
+
break
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
if len(s.clients[userID]) == 0 {
|
| 82 |
+
delete(s.clients, userID)
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
log.Printf("Client disconnected: %s", userID)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
func (s *socketService) EmitEvent(userID uuid.UUID, eventType string, payload interface{}) {
|
| 89 |
+
s.clientsMux.RLock()
|
| 90 |
+
conns, ok := s.clients[userID]
|
| 91 |
+
s.clientsMux.RUnlock()
|
| 92 |
+
|
| 93 |
+
if !ok {
|
| 94 |
+
return
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
message := map[string]interface{}{
|
| 98 |
+
"type": eventType,
|
| 99 |
+
"payload": payload,
|
| 100 |
+
"time": time.Now(),
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
for _, conn := range conns {
|
| 104 |
+
err := conn.WriteJSON(message)
|
| 105 |
+
if err != nil {
|
| 106 |
+
log.Printf("Error sending message to user %s: %v", userID, err)
|
| 107 |
+
conn.Close()
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
func (s *socketService) BroadcastEvent(eventType string, payload interface{}) {
|
| 113 |
+
s.clientsMux.RLock()
|
| 114 |
+
defer s.clientsMux.RUnlock()
|
| 115 |
+
|
| 116 |
+
message := map[string]interface{}{
|
| 117 |
+
"type": eventType,
|
| 118 |
+
"payload": payload,
|
| 119 |
+
"time": time.Now(),
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
for userID, conns := range s.clients {
|
| 123 |
+
for _, conn := range conns {
|
| 124 |
+
err := conn.WriteJSON(message)
|
| 125 |
+
if err != nil {
|
| 126 |
+
log.Printf("Error broadcasting to user %s: %v", userID, err)
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|