RyZ commited on
Commit
6e3fa96
·
1 Parent(s): 44c4b7e

feat: adding qrcode to the json

Browse files
controllers/connection_controller.go CHANGED
@@ -27,7 +27,6 @@ func NewConnectionController(connectionService services.ConnectionService) Conne
27
  }
28
 
29
  func (cc *connectionController) Connect(ctx *gin.Context) {
30
- // 1. Extract UserID from Context (set by AuthMiddleware)
31
  userID, exists := ctx.Get("user_id")
32
  if !exists {
33
  utils.SendResponse[any, any](ctx, dto.AuthResponse{}, nil, http_error.UNAUTHORIZED)
@@ -35,20 +34,20 @@ func (cc *connectionController) Connect(ctx *gin.Context) {
35
  }
36
  accountID := userID.(uuid.UUID)
37
 
38
- // 2. Bind JSON (Optional)
39
  var req dto.ConnectRequest
40
  _ = ctx.ShouldBindJSON(&req)
41
 
42
- // 3. Trigger connection in background
43
- go func() {
44
- _ = cc.connectionService.Connect(context.Background(), accountID)
45
- }()
 
46
 
47
- // 4. Return Standardized Response
48
  response := dto.ConnectResponse{
49
  Message: entity.CONNECTION_INIT_SUCCESS,
50
  AccountID: accountID,
51
  Details: entity.QR_SCAN_INSTRUCTION,
 
52
  }
53
 
54
  utils.SendResponse[dto.ConnectResponse, any](ctx, dto.AuthResponse{}, response, nil)
 
27
  }
28
 
29
  func (cc *connectionController) Connect(ctx *gin.Context) {
 
30
  userID, exists := ctx.Get("user_id")
31
  if !exists {
32
  utils.SendResponse[any, any](ctx, dto.AuthResponse{}, nil, http_error.UNAUTHORIZED)
 
34
  }
35
  accountID := userID.(uuid.UUID)
36
 
 
37
  var req dto.ConnectRequest
38
  _ = ctx.ShouldBindJSON(&req)
39
 
40
+ qrCode, err := cc.connectionService.Connect(context.Background(), accountID)
41
+ if err != nil {
42
+ utils.SendResponse[any, any](ctx, dto.AuthResponse{}, nil, err)
43
+ return
44
+ }
45
 
 
46
  response := dto.ConnectResponse{
47
  Message: entity.CONNECTION_INIT_SUCCESS,
48
  AccountID: accountID,
49
  Details: entity.QR_SCAN_INSTRUCTION,
50
+ QRCode: qrCode,
51
  }
52
 
53
  utils.SendResponse[dto.ConnectResponse, any](ctx, dto.AuthResponse{}, response, nil)
go.mod CHANGED
@@ -8,7 +8,6 @@ require (
8
  github.com/golang-jwt/jwt/v5 v5.3.0
9
  github.com/google/uuid v1.6.0
10
  github.com/joho/godotenv v1.5.1
11
- github.com/mdp/qrterminal/v3 v3.2.1
12
  go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
13
  golang.org/x/crypto v0.46.0
14
  gorm.io/driver/postgres v1.6.0
@@ -59,8 +58,6 @@ require (
59
  golang.org/x/net v0.48.0 // indirect
60
  golang.org/x/sync v0.19.0 // indirect
61
  golang.org/x/sys v0.39.0 // indirect
62
- golang.org/x/term v0.38.0 // indirect
63
  golang.org/x/text v0.32.0 // indirect
64
  google.golang.org/protobuf v1.36.11 // indirect
65
- rsc.io/qr v0.2.0 // indirect
66
  )
 
8
  github.com/golang-jwt/jwt/v5 v5.3.0
9
  github.com/google/uuid v1.6.0
10
  github.com/joho/godotenv v1.5.1
 
11
  go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
12
  golang.org/x/crypto v0.46.0
13
  gorm.io/driver/postgres v1.6.0
 
58
  golang.org/x/net v0.48.0 // indirect
59
  golang.org/x/sync v0.19.0 // indirect
60
  golang.org/x/sys v0.39.0 // indirect
 
61
  golang.org/x/text v0.32.0 // indirect
62
  google.golang.org/protobuf v1.36.11 // indirect
 
63
  )
middleware/auth_middleware.go CHANGED
@@ -49,7 +49,6 @@ func (m *authMiddleware) RequireAuth() gin.HandlerFunc {
49
  return
50
  }
51
 
52
- // Set user info in context
53
  ctx.Set("user_id", claims.UserID)
54
  ctx.Set("username", claims.Username)
55
 
 
49
  return
50
  }
51
 
 
52
  ctx.Set("user_id", claims.UserID)
53
  ctx.Set("username", claims.Username)
54
 
models/dto/connection_dto.go CHANGED
@@ -3,13 +3,14 @@ package dto
3
  import "github.com/google/uuid"
4
 
5
  type ConnectRequest struct {
6
- // Add fields here if needed in the future, e.g. force refresh
7
  }
8
 
9
  type ConnectResponse struct {
10
  Message string `json:"message"`
11
  AccountID uuid.UUID `json:"account_id"`
12
  Details string `json:"details"`
 
13
  }
14
 
15
  type ConnectionStatusResponse struct {
 
3
  import "github.com/google/uuid"
4
 
5
  type ConnectRequest struct {
6
+
7
  }
8
 
9
  type ConnectResponse struct {
10
  Message string `json:"message"`
11
  AccountID uuid.UUID `json:"account_id"`
12
  Details string `json:"details"`
13
+ QRCode string `json:"qr_code,omitempty"`
14
  }
15
 
16
  type ConnectionStatusResponse struct {
repositories/connection_repositories.go CHANGED
@@ -27,18 +27,15 @@ type connectionRepository struct {
27
  container *sqlstore.Container
28
  }
29
 
30
- // NewConnectionRepository initializes the SQLStore ONCE during app startup
31
  func NewConnectionRepository(db *gorm.DB) (ConnectionRepository, error) {
32
  sqlDB, err := db.DB()
33
  if err != nil {
34
  return nil, http_error.ERR_DB_CONNECTION_FAILED
35
  }
36
 
37
- // whatsmeow requires its own logger for the store
38
  dbLog := waLog.Stdout("Database", "INFO", true)
39
 
40
- // We use "postgres" because your connection is Postgres.
41
- // whatsmeow_device, whatsmeow_session, etc.
42
  container := sqlstore.NewWithDB(sqlDB, entity.POSTGRES_DIALECT, dbLog)
43
  if err := container.Upgrade(context.Background()); err != nil {
44
  return nil, http_error.ERR_SQLSTORE_FAILED
@@ -53,7 +50,6 @@ func NewConnectionRepository(db *gorm.DB) (ConnectionRepository, error) {
53
  func (r *connectionRepository) InitializeClient(ctx context.Context, accountID uuid.UUID) (*whatsmeow.Client, <-chan whatsmeow.QRChannelItem, error) {
54
  var acc entity.WhatsAppAccount
55
 
56
- // 1. Find or Create your GORM entity
57
  acc = entity.WhatsAppAccount{
58
  ID: accountID,
59
  AccountName: "Default Device",
@@ -65,7 +61,6 @@ func (r *connectionRepository) InitializeClient(ctx context.Context, accountID u
65
  var deviceStore *store.Device
66
  var err error
67
 
68
- // 2. Determine if we use an existing session (JID) or start a new one
69
  if acc.JID != "" {
70
  jid, parseErr := types.ParseJID(acc.JID)
71
  if parseErr != nil {
@@ -73,7 +68,6 @@ func (r *connectionRepository) InitializeClient(ctx context.Context, accountID u
73
  }
74
  deviceStore, err = r.container.GetDevice(ctx, jid)
75
  } else {
76
- // No JID means this is a fresh account/new login
77
  deviceStore = r.container.NewDevice()
78
  }
79
 
@@ -81,11 +75,9 @@ func (r *connectionRepository) InitializeClient(ctx context.Context, accountID u
81
  return nil, nil, http_error.ERR_DEVICE_STORE_FAILED
82
  }
83
 
84
- // 3. Create the whatsmeow client
85
  clientLog := waLog.Stdout(fmt.Sprintf("Client-%s", accountID), "INFO", true)
86
  client := whatsmeow.NewClient(deviceStore, clientLog)
87
 
88
- // 4. Generate QR Channel if not logged in
89
  var qrChan <-chan whatsmeow.QRChannelItem
90
  if client.Store.ID == nil {
91
  qrChan, _ = client.GetQRChannel(ctx)
@@ -95,7 +87,6 @@ func (r *connectionRepository) InitializeClient(ctx context.Context, accountID u
95
  }
96
 
97
  func (r *connectionRepository) UpdateAccountStatus(accountID uuid.UUID, jid string, isActive bool) error {
98
- // Updates both the JID (bridge) and the active status in your GORM entity
99
  return r.db.Model(&entity.WhatsAppAccount{ID: accountID}).Updates(map[string]interface{}{
100
  "jid": jid,
101
  "is_active": isActive,
@@ -112,10 +103,9 @@ func (r *connectionRepository) DeleteDevice(accountID uuid.UUID) error {
112
  jid, _ := types.ParseJID(acc.JID)
113
  device, err := r.container.GetDevice(context.Background(), jid)
114
  if err == nil && device != nil {
115
- _ = device.Delete(context.Background()) // Pass context
116
  }
117
  }
118
 
119
- // Also clear from DB
120
  return r.UpdateAccountStatus(accountID, "", false)
121
  }
 
27
  container *sqlstore.Container
28
  }
29
 
 
30
  func NewConnectionRepository(db *gorm.DB) (ConnectionRepository, error) {
31
  sqlDB, err := db.DB()
32
  if err != nil {
33
  return nil, http_error.ERR_DB_CONNECTION_FAILED
34
  }
35
 
36
+
37
  dbLog := waLog.Stdout("Database", "INFO", true)
38
 
 
 
39
  container := sqlstore.NewWithDB(sqlDB, entity.POSTGRES_DIALECT, dbLog)
40
  if err := container.Upgrade(context.Background()); err != nil {
41
  return nil, http_error.ERR_SQLSTORE_FAILED
 
50
  func (r *connectionRepository) InitializeClient(ctx context.Context, accountID uuid.UUID) (*whatsmeow.Client, <-chan whatsmeow.QRChannelItem, error) {
51
  var acc entity.WhatsAppAccount
52
 
 
53
  acc = entity.WhatsAppAccount{
54
  ID: accountID,
55
  AccountName: "Default Device",
 
61
  var deviceStore *store.Device
62
  var err error
63
 
 
64
  if acc.JID != "" {
65
  jid, parseErr := types.ParseJID(acc.JID)
66
  if parseErr != nil {
 
68
  }
69
  deviceStore, err = r.container.GetDevice(ctx, jid)
70
  } else {
 
71
  deviceStore = r.container.NewDevice()
72
  }
73
 
 
75
  return nil, nil, http_error.ERR_DEVICE_STORE_FAILED
76
  }
77
 
 
78
  clientLog := waLog.Stdout(fmt.Sprintf("Client-%s", accountID), "INFO", true)
79
  client := whatsmeow.NewClient(deviceStore, clientLog)
80
 
 
81
  var qrChan <-chan whatsmeow.QRChannelItem
82
  if client.Store.ID == nil {
83
  qrChan, _ = client.GetQRChannel(ctx)
 
87
  }
88
 
89
  func (r *connectionRepository) UpdateAccountStatus(accountID uuid.UUID, jid string, isActive bool) error {
 
90
  return r.db.Model(&entity.WhatsAppAccount{ID: accountID}).Updates(map[string]interface{}{
91
  "jid": jid,
92
  "is_active": isActive,
 
103
  jid, _ := types.ParseJID(acc.JID)
104
  device, err := r.container.GetDevice(context.Background(), jid)
105
  if err == nil && device != nil {
106
+ _ = device.Delete(context.Background())
107
  }
108
  }
109
 
 
110
  return r.UpdateAccountStatus(accountID, "", false)
111
  }
services/auth_service.go CHANGED
@@ -27,18 +27,15 @@ func NewAuthService(authRepo repositories.AuthRepository, jwtConfig config.JWTCo
27
  }
28
 
29
  func (s *authService) Register(req dto.RegisterRequest) (*dto.AuthResponse, error) {
30
- // 1. Check if user exists
31
  if _, err := s.authRepo.FindUserByUsername(req.Username); err == nil {
32
  return nil, http_error.ERR_USER_ALREADY_EXISTS
33
  }
34
 
35
- // 2. Hash Password
36
  hashedPassword, err := utils.HashPassword(req.Password)
37
  if err != nil {
38
  return nil, err
39
  }
40
 
41
- // 3. Create User
42
  user := &entity.User{
43
  Username: req.Username,
44
  Password: hashedPassword,
@@ -48,7 +45,6 @@ func (s *authService) Register(req dto.RegisterRequest) (*dto.AuthResponse, erro
48
  return nil, err
49
  }
50
 
51
- // 4. Generate Token
52
  token, err := utils.GenerateToken(user.ID, user.Username, s.jwtConfig)
53
  if err != nil {
54
  return nil, http_error.ERR_TOKEN_GENERATION_FAILED
@@ -62,18 +58,15 @@ func (s *authService) Register(req dto.RegisterRequest) (*dto.AuthResponse, erro
62
  }
63
 
64
  func (s *authService) Login(req dto.LoginRequest) (*dto.AuthResponse, error) {
65
- // 1. Find User
66
  user, err := s.authRepo.FindUserByUsername(req.Username)
67
  if err != nil {
68
  return nil, http_error.ERR_USER_NOT_FOUND
69
  }
70
 
71
- // 2. Check Password
72
  if !utils.CheckPasswordHash(req.Password, user.Password) {
73
  return nil, http_error.ERR_WRONG_PASSWORD
74
  }
75
 
76
- // 3. Generate Token
77
  token, err := utils.GenerateToken(user.ID, user.Username, s.jwtConfig)
78
  if err != nil {
79
  return nil, http_error.ERR_TOKEN_GENERATION_FAILED
 
27
  }
28
 
29
  func (s *authService) Register(req dto.RegisterRequest) (*dto.AuthResponse, error) {
 
30
  if _, err := s.authRepo.FindUserByUsername(req.Username); err == nil {
31
  return nil, http_error.ERR_USER_ALREADY_EXISTS
32
  }
33
 
 
34
  hashedPassword, err := utils.HashPassword(req.Password)
35
  if err != nil {
36
  return nil, err
37
  }
38
 
 
39
  user := &entity.User{
40
  Username: req.Username,
41
  Password: hashedPassword,
 
45
  return nil, err
46
  }
47
 
 
48
  token, err := utils.GenerateToken(user.ID, user.Username, s.jwtConfig)
49
  if err != nil {
50
  return nil, http_error.ERR_TOKEN_GENERATION_FAILED
 
58
  }
59
 
60
  func (s *authService) Login(req dto.LoginRequest) (*dto.AuthResponse, error) {
 
61
  user, err := s.authRepo.FindUserByUsername(req.Username)
62
  if err != nil {
63
  return nil, http_error.ERR_USER_NOT_FOUND
64
  }
65
 
 
66
  if !utils.CheckPasswordHash(req.Password, user.Password) {
67
  return nil, http_error.ERR_WRONG_PASSWORD
68
  }
69
 
 
70
  token, err := utils.GenerateToken(user.ID, user.Username, s.jwtConfig)
71
  if err != nil {
72
  return nil, http_error.ERR_TOKEN_GENERATION_FAILED
services/connection_service.go CHANGED
@@ -3,19 +3,18 @@ package services
3
  import (
4
  "context"
5
  "fmt"
6
- "os"
7
  "sync"
 
8
  http_error "whatsapp-backend/models/error"
9
  "whatsapp-backend/repositories"
10
 
11
  "github.com/google/uuid"
12
- "github.com/mdp/qrterminal/v3"
13
  "go.mau.fi/whatsmeow"
14
  "go.mau.fi/whatsmeow/types/events"
15
  )
16
 
17
  type ConnectionService interface {
18
- Connect(ctx context.Context, accountID uuid.UUID) error
19
  GetActiveClient(accountID uuid.UUID) (*whatsmeow.Client, error)
20
  }
21
 
@@ -30,35 +29,33 @@ func NewConnectionService(connectionRepo repositories.ConnectionRepository) Conn
30
  }
31
  }
32
 
33
- func (s *connectionService) Connect(ctx context.Context, accountID uuid.UUID) error {
34
  if existingClient, ok := s.activeClients.Load(accountID); ok {
35
  client := existingClient.(*whatsmeow.Client)
36
  if client.IsConnected() {
37
- return nil // Already connected, no action needed
38
  }
39
  }
40
 
41
- // 2. Fetch or Init Client from Repository
42
- // We assume your Repo now has a method that takes accountID and returns (client, qrChan, err)
43
- // by looking up the entity in the DB first.
44
  client, qrChan, err := s.connectionRepo.InitializeClient(ctx, accountID)
45
  if err != nil {
46
- return http_error.ERR_CLIENT_INIT_FAILED
47
  }
48
 
49
- // 3. Register Event Handlers
50
- // This is the "hook" that updates your database when the status changes
51
  client.AddEventHandler(func(evt interface{}) {
52
  s.handleWhatsAppEvent(accountID, client, evt)
53
  })
54
 
55
- // 4. Handle QR Channel in a background goroutine
 
56
  if qrChan != nil {
57
  go func() {
58
  for evt := range qrChan {
59
  if evt.Event == "code" {
60
- fmt.Printf("\n[ACCOUNT %s] SCAN THIS QR CODE:\n", accountID)
61
- qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
 
 
62
  } else {
63
  fmt.Printf("[ACCOUNT %s] Login Event: %s\n", accountID, evt.Event)
64
  }
@@ -66,16 +63,23 @@ func (s *connectionService) Connect(ctx context.Context, accountID uuid.UUID) er
66
  }()
67
  }
68
 
69
- // 5. Connect to WhatsApp Socket
70
  if err := client.Connect(); err != nil {
71
- _ = s.connectionRepo.DeleteDevice(accountID) // CLEANUP
72
- return http_error.ERR_CLIENT_CONNECT_FAILED
73
  }
74
 
75
- // 6. Store in-memory
76
- s.activeClients.Store(accountID, client)
77
-
78
- return nil
 
 
 
 
 
 
 
 
79
  }
80
 
81
  func (s *connectionService) GetActiveClient(accountID uuid.UUID) (*whatsmeow.Client, error) {
@@ -86,23 +90,18 @@ func (s *connectionService) GetActiveClient(accountID uuid.UUID) (*whatsmeow.Cli
86
  return val.(*whatsmeow.Client), nil
87
  }
88
 
89
- // handleWhatsAppEvent coordinates between the live socket and your database
90
  func (s *connectionService) handleWhatsAppEvent(accountID uuid.UUID, client *whatsmeow.Client, evt interface{}) {
91
  switch evt.(type) {
92
  case *events.Connected:
93
- // When the socket confirms connection, update the JID in your DB entity
94
  if client.Store.ID != nil {
95
  _ = s.connectionRepo.UpdateAccountStatus(accountID, client.Store.ID.String(), true)
96
  }
97
 
98
  case *events.LoggedOut:
99
- // Cleanup when the user unlinks the device from their phone
100
  s.activeClients.Delete(accountID)
101
  _ = s.connectionRepo.UpdateAccountStatus(accountID, "", false)
102
 
103
  case *events.Disconnected:
104
- // Temporary network loss - whatsmeow handles auto-reconnect,
105
- // but we might want to log this.
106
  fmt.Printf("Account %s disconnected from WhatsApp\n", accountID)
107
  }
108
  }
 
3
  import (
4
  "context"
5
  "fmt"
 
6
  "sync"
7
+ "time"
8
  http_error "whatsapp-backend/models/error"
9
  "whatsapp-backend/repositories"
10
 
11
  "github.com/google/uuid"
 
12
  "go.mau.fi/whatsmeow"
13
  "go.mau.fi/whatsmeow/types/events"
14
  )
15
 
16
  type ConnectionService interface {
17
+ Connect(ctx context.Context, accountID uuid.UUID) (string, error)
18
  GetActiveClient(accountID uuid.UUID) (*whatsmeow.Client, error)
19
  }
20
 
 
29
  }
30
  }
31
 
32
+ func (s *connectionService) Connect(ctx context.Context, accountID uuid.UUID) (string, error) {
33
  if existingClient, ok := s.activeClients.Load(accountID); ok {
34
  client := existingClient.(*whatsmeow.Client)
35
  if client.IsConnected() {
36
+ return "", nil
37
  }
38
  }
39
 
 
 
 
40
  client, qrChan, err := s.connectionRepo.InitializeClient(ctx, accountID)
41
  if err != nil {
42
+ return "", http_error.ERR_CLIENT_INIT_FAILED
43
  }
44
 
 
 
45
  client.AddEventHandler(func(evt interface{}) {
46
  s.handleWhatsAppEvent(accountID, client, evt)
47
  })
48
 
49
+ qrCodeChan := make(chan string, 1)
50
+
51
  if qrChan != nil {
52
  go func() {
53
  for evt := range qrChan {
54
  if evt.Event == "code" {
55
+ select {
56
+ case qrCodeChan <- evt.Code:
57
+ default:
58
+ }
59
  } else {
60
  fmt.Printf("[ACCOUNT %s] Login Event: %s\n", accountID, evt.Event)
61
  }
 
63
  }()
64
  }
65
 
 
66
  if err := client.Connect(); err != nil {
67
+ _ = s.connectionRepo.DeleteDevice(accountID)
68
+ return "", http_error.ERR_CLIENT_CONNECT_FAILED
69
  }
70
 
71
+ select {
72
+ case code := <-qrCodeChan:
73
+ s.activeClients.Store(accountID, client)
74
+ return code, nil
75
+ case <-time.After(3 * time.Second):
76
+ if client.IsConnected() {
77
+ s.activeClients.Store(accountID, client)
78
+ return "", nil
79
+ }
80
+ s.activeClients.Store(accountID, client)
81
+ return "", nil
82
+ }
83
  }
84
 
85
  func (s *connectionService) GetActiveClient(accountID uuid.UUID) (*whatsmeow.Client, error) {
 
90
  return val.(*whatsmeow.Client), nil
91
  }
92
 
 
93
  func (s *connectionService) handleWhatsAppEvent(accountID uuid.UUID, client *whatsmeow.Client, evt interface{}) {
94
  switch evt.(type) {
95
  case *events.Connected:
 
96
  if client.Store.ID != nil {
97
  _ = s.connectionRepo.UpdateAccountStatus(accountID, client.Store.ID.String(), true)
98
  }
99
 
100
  case *events.LoggedOut:
 
101
  s.activeClients.Delete(accountID)
102
  _ = s.connectionRepo.UpdateAccountStatus(accountID, "", false)
103
 
104
  case *events.Disconnected:
 
 
105
  fmt.Printf("Account %s disconnected from WhatsApp\n", accountID)
106
  }
107
  }