From 41ac3e0941d97daa3383fca93b34dcd55b5d838d Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Tue, 28 Nov 2023 14:26:26 -0300 Subject: [PATCH 01/16] refactor: history sync --- configs/config.go | 36 ++--- workers/history_sync_worker.go | 240 +++++++++++++++++++-------------- 2 files changed, 159 insertions(+), 117 deletions(-) diff --git a/configs/config.go b/configs/config.go index 7395d14..e86c00b 100644 --- a/configs/config.go +++ b/configs/config.go @@ -3,15 +3,15 @@ package configs import "os" type ZapMeowConfig struct { - Env string - StoragePath string - WebhookURL string - DatabaseURL string - RedisAddr string - RedisPassword string - Port string - QueueName string - MessageLimit int + Env string + StoragePath string + WebhookURL string + DatabaseURL string + RedisAddr string + RedisPassword string + Port string + QueueName string + MaxMessagesPerInstance int } func LoadConfigs() (ZapMeowConfig, error) { @@ -24,14 +24,14 @@ func LoadConfigs() (ZapMeowConfig, error) { port := os.Getenv("PORT") return ZapMeowConfig{ - Env: env, - StoragePath: storagePath, - WebhookURL: webhookURL, - DatabaseURL: databaseURL, - RedisAddr: redisAddr, - RedisPassword: redisPassword, - Port: port, - QueueName: "HISTORY_SYNC_QUEUE", - MessageLimit: 10, + Env: env, + StoragePath: storagePath, + WebhookURL: webhookURL, + DatabaseURL: databaseURL, + RedisAddr: redisAddr, + RedisPassword: redisPassword, + Port: port, + QueueName: "HISTORY_SYNC_QUEUE", + MaxMessagesPerInstance: 10, }, nil } diff --git a/workers/history_sync_worker.go b/workers/history_sync_worker.go index d4bd7a3..4fb6f48 100644 --- a/workers/history_sync_worker.go +++ b/workers/history_sync_worker.go @@ -50,117 +50,159 @@ func (q *historySyncWorker) ProcessQueue() { case <-*q.app.StopCh: return default: - data, err := queue.Dequeue() - if err != nil { - fmt.Println(err) - continue + if err := q.processHistorySync(queue); err != nil { + fmt.Println("[history sync]:", err) } + } - if data == nil { - time.Sleep(time.Second) - continue - } + time.Sleep(3 * time.Second) + } +} + +func (q *historySyncWorker) processHistorySync(queue queues.HistorySyncQueue) error { + data, err := queue.Dequeue() + if err != nil { + return err + } + + if data == nil { + return nil + } + + historySync, err := q.parseHistorySync(data.History) + if err != nil { + return err + } + + instance, err := q.wppService.GetInstance(data.InstanceID) + if err != nil { + return err + } + + account, err := q.accountService.GetAccountByInstanceID(data.InstanceID) + if err != nil { + return err + } + + if !account.WasSynced { + if err := q.accountService.DeleteAccountMessages(account.InstanceID); err != nil { + return err + } + + if err := q.accountService.UpdateAccount(account.InstanceID, map[string]interface{}{ + "WasSynced": true, + }); err != nil { + return err + } + } + + messages, err := q.processMessages(historySync, account, instance) + if err != nil { + return err + } + + if err := q.messageService.CreateMessages(&messages); err != nil { + return err + } + + return nil +} + +func (q *historySyncWorker) parseHistorySync(history []byte) (*waProto.HistorySync, error) { + var data waProto.HistorySync + if err := proto.Unmarshal(history, &data); err != nil { + return nil, err + } + return &data, nil +} + +func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *models.Account, instance *configs.Instance) ([]models.Message, error) { + var messages []models.Message + + for _, conv := range evt.GetConversations() { + chatJID, _ := types.ParseJID(conv.GetId()) + + count, err := q.messageService.CountChatMessages(account.InstanceID, chatJID.User) + if err != nil { + return nil, err + } + + if count > int64(q.app.Config.MaxMessagesPerInstance) { + continue + } - var evt waProto.HistorySync - if err := proto.Unmarshal(data.History, &evt); err != nil { - fmt.Println("proto error ", err) + historySyncMsgs := conv.GetMessages() + if historySyncMsgs == nil || len(historySyncMsgs) == 0 { + continue + } + + eventsMessage, err := q.processConversation(conv, chatJID, instance) + if err != nil { + return nil, err + } + + sort.Slice(eventsMessage, func(i, j int) bool { + return eventsMessage[i].Info.Timestamp.After(eventsMessage[j].Info.Timestamp) + }) + + maxMessages := utils.Min(q.app.Config.MaxMessagesPerInstance, len(eventsMessage)) + slice := eventsMessage[:maxMessages] + + for _, evtMessage := range slice { + parsedEvtMesage, err := q.wppService.ParseEventMessage(instance, evtMessage) + if err != nil { continue } - instance := q.app.LoadInstance(data.InstanceID) - account, err := q.accountService.GetAccountByInstanceID(data.InstanceID) + message, err := q.makeMessage(instance, parsedEvtMesage) if err != nil { - fmt.Println(err) continue } + messages = append(messages, *message) + } + } - if !account.WasSynced { - if err := q.accountService.DeleteAccountMessages(account.InstanceID); err != nil { - fmt.Println(err) - continue - } - - if err := q.accountService.UpdateAccount(account.InstanceID, map[string]interface{}{ - "WasSynced": true, - }); err != nil { - fmt.Println(err) - continue - } - } + return messages, nil +} - var messages []models.Message - for _, conv := range evt.GetConversations() { - chatJID, _ := types.ParseJID(conv.GetId()) - - count, err := q.messageService.CountChatMessages(account.InstanceID, chatJID.User) - if err != nil { - fmt.Println(err) - continue - } - - if count > int64(q.app.Config.MessageLimit) { - continue - } - - historySyncMsgs := conv.GetMessages() - if historySyncMsgs == nil || len(historySyncMsgs) == 0 { - continue - } - - var eventsMessage []*events.Message - for _, msg := range historySyncMsgs { - parsedMessage, err := instance.Client.ParseWebMessage(chatJID, msg.GetMessage()) - if err != nil { - continue - } - eventsMessage = append(eventsMessage, parsedMessage) - } - - sort.Slice(eventsMessage, func(i, j int) bool { - return eventsMessage[i].Info.Timestamp.After(eventsMessage[j].Info.Timestamp) - }) - - limit := utils.Min(q.app.Config.MessageLimit, len(eventsMessage)) - slice := eventsMessage[:limit] - - for _, eventMessage := range slice { - parsedEventMessage, err := q.wppService.ParseEventMessage(instance, eventMessage) - - message := models.Message{ - SenderJID: parsedEventMessage.SenderJID, - ChatJID: parsedEventMessage.ChatJID, - InstanceID: parsedEventMessage.InstanceID, - MessageID: parsedEventMessage.MessageID, - Timestamp: parsedEventMessage.Timestamp, - Body: parsedEventMessage.Body, - FromMe: parsedEventMessage.FromMe, - } - - if parsedEventMessage.MediaType != nil { - path, err := utils.SaveMedia( - instance.ID, - parsedEventMessage.MessageID, - *parsedEventMessage.Media, - *parsedEventMessage.Mimetype, - ) - - if err != nil { - fmt.Println(err) - } - - message.MediaType = parsedEventMessage.MediaType.String() - message.MediaPath = path - } - - if err != nil { - messages = append(messages, message) - } - } - } +func (q *historySyncWorker) processConversation(conv *waProto.Conversation, chatJID types.JID, instance *configs.Instance) ([]*events.Message, error) { + var eventsMessage []*events.Message + for _, msg := range conv.GetMessages() { + parsedMessage, err := instance.Client.ParseWebMessage(chatJID, msg.GetMessage()) + if err != nil { + continue + } + eventsMessage = append(eventsMessage, parsedMessage) + } + return eventsMessage, nil +} - if err := q.messageService.CreateMessages(&messages); err != nil { - fmt.Println(err) - } +func (q *historySyncWorker) makeMessage(instance *configs.Instance, parsedMessage services.ParsedEventMessage) (*models.Message, error) { + message := models.Message{ + SenderJID: parsedMessage.SenderJID, + ChatJID: parsedMessage.ChatJID, + InstanceID: parsedMessage.InstanceID, + MessageID: parsedMessage.MessageID, + Timestamp: parsedMessage.Timestamp, + Body: parsedMessage.Body, + FromMe: parsedMessage.FromMe, + } + + if parsedMessage.MediaType != nil { + path, err := utils.SaveMedia( + instance.ID, + parsedMessage.MessageID, + *parsedMessage.Media, + *parsedMessage.Mimetype, + ) + + if err != nil { + return nil, err } + + message.MediaType = parsedMessage.MediaType.String() + message.MediaPath = path } + + return &message, nil } From 6556bc4a247b00b5102253eebcb1837c9b7557fd Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Thu, 30 Nov 2023 20:43:34 -0300 Subject: [PATCH 02/16] refactor: improvements to error messages and logs --- configs/app.go | 5 +- configs/config.go | 4 +- configs/logger.go | 56 +++++++++++++++++ controllers/check_phones_controller.go | 2 +- controllers/get_messages_controller.go | 2 +- controllers/send_audio_message_controller.go | 2 +- controllers/send_image_message_controller.go | 2 +- controllers/send_text_message_controller.go | 2 +- go.mod | 3 +- go.sum | 9 +++ main.go | 38 ++++++------ queues/history_sync_queue.go | 10 +-- services/message_service.go | 8 ++- services/wpp_service.go | 64 ++++++++++---------- utils/get_mime_type_from_data_uri.go | 6 +- utils/make_account_storage_path.go | 5 +- utils/ping.go | 14 ----- utils/request.go | 4 -- workers/history_sync_worker.go | 8 ++- 19 files changed, 150 insertions(+), 94 deletions(-) create mode 100644 configs/logger.go delete mode 100644 utils/ping.go diff --git a/configs/app.go b/configs/app.go index 9138e95..6f33de0 100644 --- a/configs/app.go +++ b/configs/app.go @@ -10,8 +10,9 @@ import ( ) type Instance struct { - ID string - Client *whatsmeow.Client + ID string + Client *whatsmeow.Client + QrCodeRateLimit uint16 } type ZapMeow struct { diff --git a/configs/config.go b/configs/config.go index e86c00b..8f5e5e7 100644 --- a/configs/config.go +++ b/configs/config.go @@ -14,7 +14,7 @@ type ZapMeowConfig struct { MaxMessagesPerInstance int } -func LoadConfigs() (ZapMeowConfig, error) { +func LoadConfigs() ZapMeowConfig { env := os.Getenv("ENV") storagePath := os.Getenv("STORAGE_PATH") webhookURL := os.Getenv("WEBHOOK_URL") @@ -33,5 +33,5 @@ func LoadConfigs() (ZapMeowConfig, error) { Port: port, QueueName: "HISTORY_SYNC_QUEUE", MaxMessagesPerInstance: 10, - }, nil + } } diff --git a/configs/logger.go b/configs/logger.go new file mode 100644 index 0000000..20b5586 --- /dev/null +++ b/configs/logger.go @@ -0,0 +1,56 @@ +package configs + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +type Logger interface { + Trace(args ...interface{}) + Debug(args ...interface{}) + Info(args ...interface{}) + Warn(args ...interface{}) + Error(args ...interface{}) + // Calls os.Exit(1) after logging + Fatal(args ...interface{}) + // Calls panic() after logging + Panic(args ...interface{}) +} + +type logger struct { + log *logrus.Logger +} + +func NewLogger() *logger { + log := logrus.New() + log.SetFormatter(&logrus.TextFormatter{}) + log.SetOutput(os.Stdout) + return &logger{ + log: log, + } +} + +func (l *logger) Trace(args ...interface{}) { + l.log.Trace(args...) +} +func (l *logger) Debug(args ...interface{}) { + l.log.Debug(args...) +} +func (l *logger) Info(args ...interface{}) { + l.log.Info(args...) +} +func (l *logger) Warn(args ...interface{}) { + l.log.Warn(args...) +} +func (l *logger) Error(args ...interface{}) { + l.log.Error(args...) +} + +func (l *logger) Fatal(args ...interface{}) { + l.log.Fatal(args...) +} + +func (l *logger) Panic(args ...interface{}) { + l.log.Panic(args...) +} diff --git a/controllers/check_phones_controller.go b/controllers/check_phones_controller.go index 2be008b..aaf1524 100644 --- a/controllers/check_phones_controller.go +++ b/controllers/check_phones_controller.go @@ -52,7 +52,7 @@ func NewCheckPhonesController( func (p *checkPhonesController) Handler(c *gin.Context) { var body phoneCheckBody if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "Body data is invalid") + utils.RespondBadRequest(c, "Error trying to validate infos. ") return } instanceID := c.Param("instanceId") diff --git a/controllers/get_messages_controller.go b/controllers/get_messages_controller.go index 2f9f5d7..369749c 100644 --- a/controllers/get_messages_controller.go +++ b/controllers/get_messages_controller.go @@ -42,7 +42,7 @@ func (m *getMessagesController) Handler(c *gin.Context) { var body Body if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "Body data is invalid") + utils.RespondBadRequest(c, "error trying to validate infos") return } instanceID := c.Param("instanceId") diff --git a/controllers/send_audio_message_controller.go b/controllers/send_audio_message_controller.go index 1aa744b..4264bd5 100644 --- a/controllers/send_audio_message_controller.go +++ b/controllers/send_audio_message_controller.go @@ -46,7 +46,7 @@ func NewSendAudioMessageController( func (a *sendAudioMessageController) Handler(c *gin.Context) { var body audioMessageBody if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "Body data is invalid") + utils.RespondBadRequest(c, "error trying to validate infos") return } diff --git a/controllers/send_image_message_controller.go b/controllers/send_image_message_controller.go index 3dc1967..d52578b 100644 --- a/controllers/send_image_message_controller.go +++ b/controllers/send_image_message_controller.go @@ -46,7 +46,7 @@ func NewSendImageMessageController( func (i *sendImageMessageController) Handler(c *gin.Context) { var body imageMessageBody if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "Body data is invalid") + utils.RespondBadRequest(c, "error trying to validate infos") return } diff --git a/controllers/send_text_message_controller.go b/controllers/send_text_message_controller.go index bed5682..05ad204 100644 --- a/controllers/send_text_message_controller.go +++ b/controllers/send_text_message_controller.go @@ -45,7 +45,7 @@ func NewSendTextMessageController( func (t *sendTextMessageController) Handler(c *gin.Context) { var body textMessageBody if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "Body data is invalid") + utils.RespondBadRequest(c, "error trying to validate infos") return } diff --git a/go.mod b/go.mod index 8a8f0c2..fedfcb1 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.27.10 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect golang.org/x/tools v0.12.0 // indirect ) @@ -59,7 +60,7 @@ require ( golang.org/x/arch v0.5.0 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 291a836..80e6edc 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -157,6 +159,10 @@ go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c= go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I= go.mau.fi/whatsmeow v0.0.0-20230628230045-73f143bc9874 h1:UTqyzBYGw4qdRnigWc7EcCSb8YR7jno+/qychR0MR34= go.mau.fi/whatsmeow v0.0.0-20230628230045-73f143bc9874/go.mod h1:+ObGpFE6cbbY4hKc1FmQH9MVfqaemmlXGXSnwDvCOyE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -195,11 +201,14 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/main.go b/main.go index 20bbac3..8d502da 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,12 @@ package main import ( - "fmt" "sync" "zapmeow/configs" "zapmeow/models" "zapmeow/repositories" "zapmeow/routes" "zapmeow/services" - "zapmeow/utils" "zapmeow/workers" "github.com/go-redis/redis" @@ -26,15 +24,13 @@ import ( // @host localhost:8900 // @BasePath /api func main() { - err := godotenv.Load() - if err != nil { - panic("failed load .env") - } + log := configs.NewLogger() - config, err := configs.LoadConfigs() + err := godotenv.Load() if err != nil { - panic("failed get .env") + log.Fatal("Error loading dotfile. ", err) } + config := configs.LoadConfigs() // whatsmeow instances var instances sync.Map @@ -43,19 +39,19 @@ func main() { dbLog := waLog.Stdout("Database", "DEBUG", true) whatsmeowContainer, err := sqlstore.New("sqlite3", "file:"+config.DatabaseURL+"?_foreign_keys=on", dbLog) if err != nil { - panic(err) + log.Fatal("Error loading sqlite whatsmeow container. ", err) } databaseClient, err := gorm.Open(sqlite.Open(config.DatabaseURL), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { - panic("Failed to connect to database") + log.Fatal("Error creating gorm database. ", err) } db, err := databaseClient.DB() if err != nil { - panic("Failed to get database connection") + log.Fatal("Error getting gorm database. ", err) } defer db.Close() @@ -64,7 +60,7 @@ func main() { &models.Message{}, ) if err != nil { - panic(err) + log.Fatal("Error when running gorm automigrate. ", err) } // redis configs @@ -74,8 +70,8 @@ func main() { DB: 0, }) - if err := utils.Ping(redisClient); err != nil { - panic(err) + if _, err := redisClient.Ping().Result(); err != nil { + log.Fatal("Error when pinging redis. ", err) } var mutex sync.Mutex @@ -100,12 +96,13 @@ func main() { accountRepo := repositories.NewAccountRepository(app.DatabaseClient) // services - messageService := services.NewMessageService(messageRepo) + messageService := services.NewMessageService(messageRepo, log) accountService := services.NewAccountService(accountRepo, messageService) wppService := services.NewWppService( app, messageService, accountService, + log, ) // workers @@ -114,6 +111,7 @@ func main() { messageService, accountService, wppService, + log, ) r := routes.SetupRouter( @@ -123,23 +121,23 @@ func main() { accountService, ) + log.Info("Loading whatsapp instances") accounts, err := accountService.GetConnectedAccounts() - fmt.Println("loading instances...") if err != nil { - fmt.Println("[accounts]: ", err) + log.Fatal("Error getting accounts. ", err) } for _, account := range accounts { - fmt.Println("[instance]: ", account.InstanceID) + log.Info("Loading instance: ", account.InstanceID) _, err := wppService.GetInstance(account.InstanceID) if err != nil { - fmt.Println("[instance]: ", err) + log.Error("Error getting instance. ", err) } } go func() { if err := r.Run(config.Port); err != nil { - fmt.Println(err) + log.Fatal(err) } }() diff --git a/queues/history_sync_queue.go b/queues/history_sync_queue.go index 2f5f130..14cbac5 100644 --- a/queues/history_sync_queue.go +++ b/queues/history_sync_queue.go @@ -2,7 +2,6 @@ package queues import ( "encoding/json" - "fmt" "zapmeow/configs" "github.com/go-redis/redis" @@ -16,6 +15,7 @@ type HistorySyncQueueData struct { type historySyncQueue struct { client *redis.Client app *configs.ZapMeow + log configs.Logger } type HistorySyncQueue interface { @@ -23,15 +23,17 @@ type HistorySyncQueue interface { Dequeue() (*HistorySyncQueueData, error) } -func NewHistorySyncQueue(app *configs.ZapMeow) *historySyncQueue { +func NewHistorySyncQueue(app *configs.ZapMeow, log configs.Logger) *historySyncQueue { return &historySyncQueue{ app: app, + log: log, } } func (q *historySyncQueue) Enqueue(item HistorySyncQueueData) error { jsonData, err := json.Marshal(item) if err != nil { + q.log.Error("Error marshal history sync", err) return err } @@ -44,7 +46,7 @@ func (q *historySyncQueue) Dequeue() (*HistorySyncQueueData, error) { if err == redis.Nil { return nil, nil } else { - fmt.Printf("Error dequeuing item: %s", err) + q.log.Error("Error dequeuing history sync", err) return nil, err } } @@ -53,7 +55,7 @@ func (q *historySyncQueue) Dequeue() (*HistorySyncQueueData, error) { err = json.Unmarshal(result, &data) if err != nil { - fmt.Printf("Error unmarshal item: %s", err) + q.log.Error("Error unmarshal history sync.", err) return nil, err } diff --git a/services/message_service.go b/services/message_service.go index fd88520..e7da0db 100644 --- a/services/message_service.go +++ b/services/message_service.go @@ -2,11 +2,11 @@ package services import ( "encoding/base64" - "fmt" "io/ioutil" "mime" "path/filepath" "time" + "zapmeow/configs" "zapmeow/models" "zapmeow/repositories" ) @@ -37,11 +37,13 @@ type Message struct { type messageService struct { messageRep repositories.MessageRepository + log configs.Logger } -func NewMessageService(messageRep repositories.MessageRepository) *messageService { +func NewMessageService(messageRep repositories.MessageRepository, log configs.Logger) *messageService { return &messageService{ messageRep: messageRep, + log: log, } } @@ -80,7 +82,7 @@ func (m *messageService) ToJSON(message models.Message) Message { if message.MediaType != "" { data, err := ioutil.ReadFile(message.MediaPath) if err != nil { - fmt.Println(err) + m.log.Error("Error reading the file. ", err) } else { mimetype := mime.TypeByExtension(filepath.Ext(message.MediaPath)) base64 := base64.StdEncoding.EncodeToString(data) diff --git a/services/wpp_service.go b/services/wpp_service.go index 2216649..0865767 100644 --- a/services/wpp_service.go +++ b/services/wpp_service.go @@ -3,7 +3,6 @@ package services import ( "context" "errors" - "fmt" "os" "time" "zapmeow/configs" @@ -25,6 +24,7 @@ type wppService struct { app *configs.ZapMeow messageService MessageService accountService AccountService + log configs.Logger } type ContactInfo struct { @@ -105,11 +105,13 @@ func NewWppService( app *configs.ZapMeow, messageService MessageService, accountService AccountService, + log configs.Logger, ) *wppService { return &wppService{ app: app, messageService: messageService, accountService: accountService, + log: log, } } @@ -126,8 +128,9 @@ func (w *wppService) GetInstance(instanceID string) (*configs.Instance, error) { } w.app.StoreInstance(instanceID, &configs.Instance{ - ID: instanceID, - Client: client, + ID: instanceID, + Client: client, + QrCodeRateLimit: 10, }) instance = w.app.LoadInstance(instanceID) @@ -159,11 +162,11 @@ func (w *wppService) GetAuthenticatedInstance(instanceID string) (*configs.Insta } if !instance.Client.IsConnected() { - return nil, errors.New("instance not connected") + return nil, errors.New("unconnected instance") } if !instance.Client.IsLoggedIn() { - return nil, errors.New("inauthenticated instance") + return nil, errors.New("unauthenticated instance") } return instance, nil @@ -175,7 +178,6 @@ func (w *wppService) SendTextMessage(instanceID string, jid types.JID, text stri Text: &text, }, } - return w.SendMessage(instanceID, jid, message) } @@ -184,7 +186,6 @@ func (w *wppService) SendAudioMessage(instanceID string, jid types.JID, audioURL if err != nil { return SendMessageResponse{}, err } - message := &waProto.Message{ AudioMessage: &waProto.AudioMessage{ Ptt: proto.Bool(true), @@ -197,7 +198,6 @@ func (w *wppService) SendAudioMessage(instanceID string, jid types.JID, audioURL FileLength: proto.Uint64(uint64(len(audioURL.Data))), }, } - return w.SendMessage(instanceID, jid, message) } @@ -206,7 +206,6 @@ func (w *wppService) SendImageMessage(instanceID string, jid types.JID, imageURL if err != nil { return SendMessageResponse{}, err } - message := &waProto.Message{ ImageMessage: &waProto.ImageMessage{ Url: proto.String(uploaded.URL), @@ -218,7 +217,6 @@ func (w *wppService) SendImageMessage(instanceID string, jid types.JID, imageURL FileLength: proto.Uint64(uint64(len(imageURL.Data))), }, } - return w.SendMessage(instanceID, jid, message) } @@ -511,45 +509,49 @@ func (w *wppService) qrcode(instanceID string) { qrChan, err := instance.Client.GetQRChannel(context.Background()) if err != nil { if !errors.Is(err, whatsmeow.ErrQRStoreContainsID) { - if w.app.Config.Env != "production" { - fmt.Println("failed to get qr channel") - } + w.log.Error("Failed to get qr channel. ", err) } } else { err = instance.Client.Connect() if err != nil { - fmt.Println("[qrcode]: ", err) + w.log.Error("Failed to connect client to WhatsApp websocket. ", err) return } + for evt := range qrChan { + if instance.QrCodeRateLimit == 0 { + err := w.destroyInstance(instanceID) + if err != nil { + w.log.Error("Failed to destroy instance. ", err) + } + return + } + switch evt.Event { case "success": return case "timeout": { - // w.app.Mutex.Lock() - // defer w.app.Mutex.Unlock() err := w.accountService.UpdateAccount(instanceID, map[string]interface{}{ "QrCode": "", "Status": "TIMEOUT", }) if err != nil { - fmt.Println("[qrcode]: ", err) + w.log.Error("Failed to update account. ", err) } w.app.DeleteInstance(instanceID) } case "code": { - // w.app.Mutex.Lock() - // defer w.app.Mutex.Unlock() - w.accountService.UpdateAccount(instanceID, map[string]interface{}{ + instance.QrCodeRateLimit -= 1 + err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ "QrCode": evt.Code, "Status": "UNPAIRED", "WasSynced": false, }) if err != nil { - fmt.Println("[qrcode]: ", err) + w.log.Error("Failed to update account. ", err) } } } @@ -574,14 +576,14 @@ func (w *wppService) eventHandler(instanceID string, rawEvt interface{}) { func (w *wppService) handleHistorySync(instanceID string, evt *events.HistorySync) { history, _ := proto.Marshal(evt.Data) - queue := queues.NewHistorySyncQueue(w.app) + queue := queues.NewHistorySyncQueue(w.app, w.log) err := queue.Enqueue(queues.HistorySyncQueueData{ History: history, InstanceID: instanceID, }) if err != nil { - fmt.Println("Error adding item to queue: ", err) + w.log.Error("Failed to add history sync to queue. ", err) } } @@ -600,22 +602,21 @@ func (w *wppService) handleConnected(instanceID string) { }) if err != nil { - fmt.Println("Error creating account:", err) - return + w.log.Error("Failed to update account. ", err) } } func (w *wppService) handleLoggedOut(instanceID string) { err := w.destroyInstance(instanceID) if err != nil { - fmt.Println("Error", err) + w.log.Error(err) } err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ "Status": "UNPAIRED", }) if err != nil { - fmt.Println("Error", err) + w.log.Error("Failed to update account. ", err) } } @@ -624,6 +625,7 @@ func (w *wppService) handleMessage(instanceId string, evt *events.Message) { parsedEventMessage, err := w.ParseEventMessage(instance, evt) if err != nil { + w.log.Error(err) return } @@ -646,7 +648,7 @@ func (w *wppService) handleMessage(instanceId string, evt *events.Message) { ) if err != nil { - fmt.Println(err) + w.log.Error("Failed to save media. ", err) } message.MediaType = parsedEventMessage.MediaType.String() @@ -655,7 +657,8 @@ func (w *wppService) handleMessage(instanceId string, evt *events.Message) { err = w.messageService.CreateMessage(&message) if err != nil { - fmt.Println(err) + w.log.Error("Failed to create message. ", err) + return } body := map[string]interface{}{ @@ -664,9 +667,8 @@ func (w *wppService) handleMessage(instanceId string, evt *events.Message) { } err = utils.Request(w.app.Config.WebhookURL, body) - if err != nil { - fmt.Println("Error when send request:", err) + w.log.Error("Failed to send webhook request. ", err) } } diff --git a/utils/get_mime_type_from_data_uri.go b/utils/get_mime_type_from_data_uri.go index e4534eb..b3ff702 100644 --- a/utils/get_mime_type_from_data_uri.go +++ b/utils/get_mime_type_from_data_uri.go @@ -1,19 +1,19 @@ package utils import ( - "fmt" + "errors" "strings" ) func GetMimeTypeFromDataURI(dataURI string) (string, error) { components := strings.Split(dataURI, ",") if len(components) < 2 { - return "", fmt.Errorf("Invalid Data URI") + return "", errors.New("invalid data uri") } mimeTypeComponents := strings.Split(components[0], ";") if len(mimeTypeComponents) < 2 { - return "", fmt.Errorf("Invalid Data URI: MIME type not found") + return "", errors.New("mime type not found") } mimeType := strings.TrimPrefix(mimeTypeComponents[0], "data:") diff --git a/utils/make_account_storage_path.go b/utils/make_account_storage_path.go index 099ae65..77e7734 100644 --- a/utils/make_account_storage_path.go +++ b/utils/make_account_storage_path.go @@ -2,9 +2,10 @@ package utils import ( "fmt" - "os" + "zapmeow/configs" ) func MakeAccountStoragePath(instanceID string) string { - return fmt.Sprintf("%s/instance_%s", os.Getenv("STORAGE_PATH"), instanceID) + config := configs.LoadConfigs() + return fmt.Sprintf("%s/instance_%s", config.StoragePath, instanceID) } diff --git a/utils/ping.go b/utils/ping.go deleted file mode 100644 index 016d4e4..0000000 --- a/utils/ping.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "github.com/go-redis/redis" -) - -func Ping(client *redis.Client) error { - _, err := client.Ping().Result() - if err != nil { - return err - } - - return nil -} diff --git a/utils/request.go b/utils/request.go index 0f7204b..9388d37 100644 --- a/utils/request.go +++ b/utils/request.go @@ -3,7 +3,6 @@ package utils import ( "bytes" "encoding/json" - "fmt" "net/http" ) @@ -11,13 +10,11 @@ func Request(url string, data map[string]interface{}) error { body, err := json.Marshal(data) if err != nil { - fmt.Println("Error when serializing the map in JSON:", err) return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { - fmt.Println("Error creating request:", err) return err } req.Header.Set("Content-Type", "application/json") @@ -25,7 +22,6 @@ func Request(url string, data map[string]interface{}) error { client := &http.Client{} resp, err := client.Do(req) if err != nil { - fmt.Println("Error sending request:", err) return err } diff --git a/workers/history_sync_worker.go b/workers/history_sync_worker.go index 4fb6f48..e418850 100644 --- a/workers/history_sync_worker.go +++ b/workers/history_sync_worker.go @@ -1,7 +1,6 @@ package workers import ( - "fmt" "sort" "time" "zapmeow/configs" @@ -21,6 +20,7 @@ type historySyncWorker struct { messageService services.MessageService accountService services.AccountService wppService services.WppService + log configs.Logger } type HistorySyncWorker interface { @@ -32,17 +32,19 @@ func NewHistorySyncWorker( messageService services.MessageService, accountService services.AccountService, wppService services.WppService, + log configs.Logger, ) *historySyncWorker { return &historySyncWorker{ messageService: messageService, accountService: accountService, wppService: wppService, app: app, + log: log, } } func (q *historySyncWorker) ProcessQueue() { - queue := queues.NewHistorySyncQueue(q.app) + queue := queues.NewHistorySyncQueue(q.app, q.log) defer q.app.Wg.Done() for { @@ -51,7 +53,7 @@ func (q *historySyncWorker) ProcessQueue() { return default: if err := q.processHistorySync(queue); err != nil { - fmt.Println("[history sync]:", err) + q.log.Error("Error processing history sync. ", err) } } From 5ae38383c61c44da403c0cf093f9e29ffd0e6d9d Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Thu, 30 Nov 2023 21:12:54 -0300 Subject: [PATCH 03/16] update --- go.mod | 2 +- go.sum | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/go.mod b/go.mod index fedfcb1..878f3c8 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/gin-gonic/gin v1.9.1 github.com/joho/godotenv v1.5.1 + github.com/sirupsen/logrus v1.9.3 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.2 @@ -31,7 +32,6 @@ require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.27.10 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect golang.org/x/tools v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index 80e6edc..78a59f5 100644 --- a/go.sum +++ b/go.sum @@ -159,10 +159,6 @@ go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c= go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I= go.mau.fi/whatsmeow v0.0.0-20230628230045-73f143bc9874 h1:UTqyzBYGw4qdRnigWc7EcCSb8YR7jno+/qychR0MR34= go.mau.fi/whatsmeow v0.0.0-20230628230045-73f143bc9874/go.mod h1:+ObGpFE6cbbY4hKc1FmQH9MVfqaemmlXGXSnwDvCOyE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -205,8 +201,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From 9e4984b26a75cdd55827552e4cd034deddfb4b2e Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Fri, 1 Dec 2023 13:56:44 -0300 Subject: [PATCH 04/16] fix: log formatting --- configs/logger.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configs/logger.go b/configs/logger.go index 20b5586..5ae0847 100644 --- a/configs/logger.go +++ b/configs/logger.go @@ -24,7 +24,10 @@ type logger struct { func NewLogger() *logger { log := logrus.New() - log.SetFormatter(&logrus.TextFormatter{}) + log.SetFormatter(&logrus.TextFormatter{ + DisableColors: true, + FullTimestamp: true, + }) log.SetOutput(os.Stdout) return &logger{ log: log, From bf0da4262a8e7f8fd43f82e4e3923765da672c9b Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Fri, 1 Dec 2023 14:13:20 -0300 Subject: [PATCH 05/16] refactor: change environment config --- .env.example | 2 +- configs/config.go | 21 ++++++++++++++++++--- routes/routes.go | 2 +- services/wpp_service.go | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 3f4bf7d..3c1bfcd 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -ENV=development +ENVIRONMENT=development PORT=:8900 REDIS_ADDR=localhost:6379 REDIS_PASSWORD= diff --git a/configs/config.go b/configs/config.go index 8f5e5e7..475ebae 100644 --- a/configs/config.go +++ b/configs/config.go @@ -2,8 +2,15 @@ package configs import "os" +type Environment = uint + +const ( + Development Environment = iota + Production +) + type ZapMeowConfig struct { - Env string + Environment Environment StoragePath string WebhookURL string DatabaseURL string @@ -15,16 +22,16 @@ type ZapMeowConfig struct { } func LoadConfigs() ZapMeowConfig { - env := os.Getenv("ENV") storagePath := os.Getenv("STORAGE_PATH") webhookURL := os.Getenv("WEBHOOK_URL") databaseURL := os.Getenv("DATABASE_PATH") redisAddr := os.Getenv("REDIS_ADDR") redisPassword := os.Getenv("REDIS_PASSWORD") port := os.Getenv("PORT") + env := getEnvironment() return ZapMeowConfig{ - Env: env, + Environment: env, StoragePath: storagePath, WebhookURL: webhookURL, DatabaseURL: databaseURL, @@ -35,3 +42,11 @@ func LoadConfigs() ZapMeowConfig { MaxMessagesPerInstance: 10, } } + +func getEnvironment() Environment { + env := os.Getenv("ENVIRONMENT") + if env == "production" { + return Production + } + return Development +} diff --git a/routes/routes.go b/routes/routes.go index 57afa17..6a4e5b1 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -21,7 +21,7 @@ func SetupRouter( docs.SwaggerInfo.BasePath = "/api" var router *gin.Engine - if app.Config.Env == "production" { + if app.Config.Environment == configs.Development { router = gin.New() } else { router = gin.Default() diff --git a/services/wpp_service.go b/services/wpp_service.go index 0865767..b5c0089 100644 --- a/services/wpp_service.go +++ b/services/wpp_service.go @@ -673,7 +673,7 @@ func (w *wppService) handleMessage(instanceId string, evt *events.Message) { } func createClient(config configs.ZapMeowConfig, deviceStore *store.Device) *whatsmeow.Client { - if config.Env == "production" { + if config.Environment == configs.Production { return whatsmeow.NewClient(deviceStore, nil) } log := waLog.Stdout("Client", "DEBUG", true) From 7fdd2a6dacfa6b218b4796477d96b0cd25f2e518 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 25 Dec 2023 14:41:06 -0300 Subject: [PATCH 06/16] architecture refactoring --- .env.example | 4 +- .gitignore | 2 +- Dockerfile | 4 +- api/handler/check_phones_handler.go | 65 ++ api/handler/get_contract_info_handler.go | 68 ++ api/handler/get_messages_handler.go | 75 ++ api/handler/get_profile_info_handler.go | 66 ++ api/handler/get_qrcode_handler.go | 69 ++ api/handler/get_status_handler.go | 76 ++ api/handler/logout_handler.go | 59 ++ api/handler/send_audio_message_handler.go | 122 ++++ api/handler/send_image_message_handler.go | 122 ++++ api/handler/send_text_message_handler.go | 93 +++ .../helper}/get_mime_type_from_data_uri.go | 2 +- .../helper}/make_account_storage_path.go | 6 +- {utils => api/helper}/make_jid.go | 2 +- {utils => api/helper}/min.go | 2 +- {utils => api/helper}/save_media.go | 2 +- {models => api/model}/account.go | 2 +- {models => api/model}/message.go | 2 +- {queues => api/queue}/history_sync_queue.go | 40 +- api/repository/account_repository.go | 58 ++ api/repository/message_repository.go | 53 ++ api/response/message_response.go | 59 ++ api/response/response.go | 65 ++ api/route/routes.go | 90 +++ {services => api/service}/account_service.go | 26 +- api/service/account_service_test.go | 1 + api/service/message_service.go | 44 ++ api/service/message_service_test.go | 1 + api/service/whatsapp_service.go | 334 +++++++++ api/service/whatsapp_service_test.go | 14 + cmd/server/main.go | 116 +++ {configs => config}/config.go | 10 +- configs/app.go | 65 -- configs/logger.go | 59 -- controllers/check_phones_controller.go | 96 --- controllers/get_contract_info_controller.go | 55 -- controllers/get_messages_controller.go | 73 -- controllers/get_profile_info_controller.go | 59 -- controllers/get_qrcode_controller.go | 69 -- controllers/get_status_controller.go | 76 -- controllers/logout_controller.go | 47 -- controllers/send_audio_message_controller.go | 109 --- controllers/send_image_message_controller.go | 109 --- controllers/send_text_message_controller.go | 84 --- main.go | 150 ---- pkg/database/database.go | 39 + utils/request.go => pkg/http/http.go | 6 +- pkg/logger/logger.go | 62 ++ pkg/queue/redis.go | 44 ++ pkg/whatsapp/whatsapp.go | 488 +++++++++++++ pkg/zapmeow/zapmeow.go | 55 ++ repositories/account_repository.go | 57 -- repositories/message_repository.go | 54 -- routes/routes.go | 87 --- services/message_service.go | 100 --- services/wpp_service.go | 681 ------------------ utils/response.go | 40 - {workers => worker}/history_sync_worker.go | 66 +- 60 files changed, 2425 insertions(+), 2159 deletions(-) create mode 100644 api/handler/check_phones_handler.go create mode 100644 api/handler/get_contract_info_handler.go create mode 100644 api/handler/get_messages_handler.go create mode 100644 api/handler/get_profile_info_handler.go create mode 100644 api/handler/get_qrcode_handler.go create mode 100644 api/handler/get_status_handler.go create mode 100644 api/handler/logout_handler.go create mode 100644 api/handler/send_audio_message_handler.go create mode 100644 api/handler/send_image_message_handler.go create mode 100644 api/handler/send_text_message_handler.go rename {utils => api/helper}/get_mime_type_from_data_uri.go (97%) rename {utils => api/helper}/make_account_storage_path.go (69%) rename {utils => api/helper}/make_jid.go (98%) rename {utils => api/helper}/min.go (81%) rename {utils => api/helper}/save_media.go (97%) rename {models => api/model}/account.go (93%) rename {models => api/model}/message.go (95%) rename {queues => api/queue}/history_sync_queue.go (52%) create mode 100644 api/repository/account_repository.go create mode 100644 api/repository/message_repository.go create mode 100644 api/response/message_response.go create mode 100644 api/response/response.go create mode 100644 api/route/routes.go rename {services => api/service}/account_service.go (66%) create mode 100644 api/service/account_service_test.go create mode 100644 api/service/message_service.go create mode 100644 api/service/message_service_test.go create mode 100644 api/service/whatsapp_service.go create mode 100644 api/service/whatsapp_service_test.go create mode 100644 cmd/server/main.go rename {configs => config}/config.go (87%) delete mode 100644 configs/app.go delete mode 100644 configs/logger.go delete mode 100644 controllers/check_phones_controller.go delete mode 100644 controllers/get_contract_info_controller.go delete mode 100644 controllers/get_messages_controller.go delete mode 100644 controllers/get_profile_info_controller.go delete mode 100644 controllers/get_qrcode_controller.go delete mode 100644 controllers/get_status_controller.go delete mode 100644 controllers/logout_controller.go delete mode 100644 controllers/send_audio_message_controller.go delete mode 100644 controllers/send_image_message_controller.go delete mode 100644 controllers/send_text_message_controller.go delete mode 100644 main.go create mode 100644 pkg/database/database.go rename utils/request.go => pkg/http/http.go (83%) create mode 100644 pkg/logger/logger.go create mode 100644 pkg/queue/redis.go create mode 100644 pkg/whatsapp/whatsapp.go create mode 100644 pkg/zapmeow/zapmeow.go delete mode 100644 repositories/account_repository.go delete mode 100644 repositories/message_repository.go delete mode 100644 routes/routes.go delete mode 100644 services/message_service.go delete mode 100644 services/wpp_service.go delete mode 100644 utils/response.go rename {workers => worker}/history_sync_worker.go (71%) diff --git a/.env.example b/.env.example index 3c1bfcd..9a5df51 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,6 @@ ENVIRONMENT=development PORT=:8900 REDIS_ADDR=localhost:6379 REDIS_PASSWORD= -DATABASE_PATH=data.db -STORAGE_PATH=storage +DATABASE_PATH=.zapmeow/zapmeow.db +STORAGE_PATH=.zapmeow/storage WEBHOOK_URL=http://localhost:3000/api/whatsapp/message diff --git a/.gitignore b/.gitignore index 4c956a5..9f46792 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ storage .env .env.production main -zapmeow +.zapmeow diff --git a/Dockerfile b/Dockerfile index 665751c..6a0390d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ RUN ls -la RUN go mod download ENV CGO_ENABLED=1 -RUN go build -o main . +RUN go build -o server cmd/server/main.go EXPOSE 8900 -CMD ["./main"] +CMD ["./server"] diff --git a/api/handler/check_phones_handler.go b/api/handler/check_phones_handler.go new file mode 100644 index 0000000..d38fa0a --- /dev/null +++ b/api/handler/check_phones_handler.go @@ -0,0 +1,65 @@ +package handler + +import ( + "net/http" + "zapmeow/api/response" + "zapmeow/api/service" + "zapmeow/pkg/whatsapp" + + "github.com/gin-gonic/gin" +) + +type getCheckPhonesBody struct { + Phones []string `json:"phones"` +} + +type getCheckPhonesResponse struct { + Phones []whatsapp.IsOnWhatsAppResponse `json:"phones"` +} + +type checkPhonesHandler struct { + whatsAppService service.WhatsAppService +} + +func NewCheckPhonesHandler( + whatsAppService service.WhatsAppService, +) *checkPhonesHandler { + return &checkPhonesHandler{ + whatsAppService: whatsAppService, + } +} + +// Check Phones on WhatsApp +// @Summary Check Phones on WhatsApp +// @Description Verifies if the phone numbers in the provided list are registered WhatsApp users. +// @Tags WhatsApp Phone Verification +// @Param instanceId path string true "Instance ID" +// @Param data body getCheckPhonesBody true "Phone list" +// @Accept json +// @Produce json +// @Success 200 {object} getCheckPhonesResponse "List of verified numbers" +// @Router /{instanceId}/check/phones [post] +func (h *checkPhonesHandler) Handler(c *gin.Context) { + var body getCheckPhonesBody + if err := c.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + return + } + + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + phones, err := h.whatsAppService.IsOnWhatsApp(instance, body.Phones) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, getCheckPhonesResponse{ + Phones: phones, + }) +} diff --git a/api/handler/get_contract_info_handler.go b/api/handler/get_contract_info_handler.go new file mode 100644 index 0000000..eec2fcc --- /dev/null +++ b/api/handler/get_contract_info_handler.go @@ -0,0 +1,68 @@ +package handler + +import ( + "net/http" + "zapmeow/api/helper" + "zapmeow/api/response" + "zapmeow/api/service" + "zapmeow/pkg/whatsapp" + + "github.com/gin-gonic/gin" +) + +type contactInfoResponse struct { + Info whatsapp.ContactInfo `json:"info"` +} + +type getContactInfoHandler struct { + whatsAppService service.WhatsAppService +} + +func NewGetContactInfoHandler( + whatsAppService service.WhatsAppService, +) *getContactInfoHandler { + return &getContactInfoHandler{ + whatsAppService: whatsAppService, + } +} + +// Get Contact Information +// @Summary Get Contact Information +// @Description Retrieves contact information. +// @Tags WhatsApp Contact +// @Param instanceId path string true "Instance ID" +// @Param phone query string true "Phone" +// @Accept json +// @Produce json +// @Success 200 {object} contactInfoResponse "Contact Information" +// @Router /{instanceId}/contact/info [get] +func (h *getContactInfoHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if !h.whatsAppService.IsAuthenticated(instance) { + response.ErrorResponse(c, http.StatusUnauthorized, "unautenticated") + return + } + + phone := c.Query("phone") + jid, ok := helper.MakeJID(phone) + if !ok { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + return + } + + info, err := h.whatsAppService.GetContactInfo(instance, jid) + if err != nil || info == nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, contactInfoResponse{ + Info: *info, + }) +} diff --git a/api/handler/get_messages_handler.go b/api/handler/get_messages_handler.go new file mode 100644 index 0000000..dbbbac4 --- /dev/null +++ b/api/handler/get_messages_handler.go @@ -0,0 +1,75 @@ +package handler + +import ( + "net/http" + "zapmeow/api/response" + "zapmeow/api/service" + + "github.com/gin-gonic/gin" +) + +type getMessagesBody struct { + Phone string `json:"phone"` +} + +type getMessagesResponse struct { + Messages []response.Message `json:"messages"` +} + +type getMessagesHandler struct { + whatsAppService service.WhatsAppService + messageService service.MessageService +} + +func NewGetMessagesHandler( + whatsAppService service.WhatsAppService, + messageService service.MessageService, +) *getMessagesHandler { + return &getMessagesHandler{ + whatsAppService: whatsAppService, + messageService: messageService, + } +} + +// Get WhatsApp Chat Messages +// @Summary Get WhatsApp Chat Messages +// @Description Returns chat messages from the specified WhatsApp instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body getMessagesBody true "Phone" +// @Accept json +// @Produce json +// @Success 200 {object} getMessagesResponse "List of chat messages" +// @Router /{instanceId}/chat/messages [post] +func (h *getMessagesHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if !h.whatsAppService.IsAuthenticated(instance) { + response.ErrorResponse(c, http.StatusUnauthorized, "unautenticated") + return + } + + var body getMessagesBody + if err := c.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + return + } + + messages, err := h.messageService.GetChatMessages( + instanceID, + body.Phone, + ) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, getMessagesResponse{ + Messages: response.NewMessagesResponse(messages), + }) +} diff --git a/api/handler/get_profile_info_handler.go b/api/handler/get_profile_info_handler.go new file mode 100644 index 0000000..3b7039f --- /dev/null +++ b/api/handler/get_profile_info_handler.go @@ -0,0 +1,66 @@ +package handler + +import ( + "net/http" + "zapmeow/api/helper" + "zapmeow/api/response" + "zapmeow/api/service" + "zapmeow/pkg/whatsapp" + + "github.com/gin-gonic/gin" +) + +type getProfileInfoResponse struct { + Info whatsapp.ContactInfo `json:"info"` +} + +type getProfileInfoHandler struct { + whatsAppService service.WhatsAppService +} + +func NewGetProfileInfoHandler( + whatsAppService service.WhatsAppService, +) *getProfileInfoHandler { + return &getProfileInfoHandler{ + whatsAppService: whatsAppService, + } +} + +// Get Profile Information +// @Summary Get Profile Information +// @Description Retrieves profile information. +// @Tags WhatsApp Profile +// @Param instanceId path string true "Instance ID" +// @Accept json +// @Produce json +// @Success 200 {object} getProfileInfoResponse "Profile Information" +// @Router /{instanceId}/profile [get] +func (h *getProfileInfoHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if !h.whatsAppService.IsAuthenticated(instance) { + response.ErrorResponse(c, http.StatusUnauthorized, "unautenticated") + return + } + + jid, ok := helper.MakeJID(instance.Client.Store.ID.User) + if !ok { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + return + } + + info, err := h.whatsAppService.GetContactInfo(instance, jid) + if err != nil || info == nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, getProfileInfoResponse{ + Info: *info, + }) +} diff --git a/api/handler/get_qrcode_handler.go b/api/handler/get_qrcode_handler.go new file mode 100644 index 0000000..6a1fa57 --- /dev/null +++ b/api/handler/get_qrcode_handler.go @@ -0,0 +1,69 @@ +package handler + +import ( + "net/http" + "zapmeow/api/response" + "zapmeow/api/service" + "zapmeow/pkg/zapmeow" + + "github.com/gin-gonic/gin" +) + +type getQrCodeResponse struct { + QrCode string `json:"qrcode"` +} + +type getQrCodeHandler struct { + app *zapmeow.ZapMeow + whatsAppService service.WhatsAppService + messageService service.MessageService + accountService service.AccountService +} + +func NewGetQrCodeHandler( + app *zapmeow.ZapMeow, + whatsAppService service.WhatsAppService, + messageService service.MessageService, + accountService service.AccountService, +) *getQrCodeHandler { + return &getQrCodeHandler{ + app: app, + whatsAppService: whatsAppService, + messageService: messageService, + accountService: accountService, + } +} + +// Get QR Code for WhatsApp Login +// @Summary Get WhatsApp QR Code +// @Description Returns a QR code to initiate WhatsApp login. +// @Tags WhatsApp Login +// @Param instanceId path string true "Instance ID" +// @Produce json +// @Success 200 {object} getQrCodeResponse "QR Code" +// @Router /{instanceId}/qrcode [get] +func (h *getQrCodeHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + _, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + h.app.Mutex.Lock() + defer h.app.Mutex.Unlock() + account, err := h.accountService.GetAccountByInstanceID(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if account == nil { + response.ErrorResponse(c, http.StatusInternalServerError, "Account not foun") + return + } + + response.Response(c, http.StatusOK, getQrCodeResponse{ + QrCode: account.QrCode, + }) +} diff --git a/api/handler/get_status_handler.go b/api/handler/get_status_handler.go new file mode 100644 index 0000000..97e060d --- /dev/null +++ b/api/handler/get_status_handler.go @@ -0,0 +1,76 @@ +package handler + +import ( + "net/http" + "zapmeow/api/response" + "zapmeow/api/service" + "zapmeow/pkg/zapmeow" + + "github.com/gin-gonic/gin" +) + +type getStatusResponse struct { + Status string `json:"status"` +} + +type getStatusHandler struct { + app *zapmeow.ZapMeow + whatsAppService service.WhatsAppService + accountService service.AccountService +} + +func NewGetStatusHandler( + app *zapmeow.ZapMeow, + whatsAppService service.WhatsAppService, + accountService service.AccountService, +) *getStatusHandler { + return &getStatusHandler{ + app: app, + whatsAppService: whatsAppService, + accountService: accountService, + } +} + +// Get WhatsApp Instance Status +// @Summary Get WhatsApp Instance Status +// @Description Returns the status of the specified WhatsApp instance. +// @Tags WhatsApp Status +// @Param instanceId path string true "Instance ID" +// @Accept json +// @Produce json +// @Success 200 {object} getStatusResponse "Status Response" +// @Router /{instanceId}/status [get] +func (h *getStatusHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + h.app.Mutex.Lock() + defer h.app.Mutex.Unlock() + account, err := h.accountService.GetAccountByInstanceID(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if account == nil { + response.ErrorResponse(c, http.StatusInternalServerError, "Account not found") + return + } + + var status = account.Status + if !instance.Client.IsConnected() { + status = "DISCONNECTED" + } + + if status == "CONNECTED" && !instance.Client.IsLoggedIn() { + status = "UNPAIRED" + } + + response.Response(c, http.StatusOK, getStatusResponse{ + Status: status, + }) +} diff --git a/api/handler/logout_handler.go b/api/handler/logout_handler.go new file mode 100644 index 0000000..8a35450 --- /dev/null +++ b/api/handler/logout_handler.go @@ -0,0 +1,59 @@ +package handler + +import ( + "net/http" + "zapmeow/api/response" + "zapmeow/api/service" + "zapmeow/pkg/zapmeow" + + "github.com/gin-gonic/gin" +) + +type logoutHandler struct { + app *zapmeow.ZapMeow + whatsAppService service.WhatsAppService + accountService service.AccountService +} + +func NewLogoutHandler( + app *zapmeow.ZapMeow, + whatsAppService service.WhatsAppService, + accountService service.AccountService, +) *logoutHandler { + return &logoutHandler{ + app: app, + whatsAppService: whatsAppService, + accountService: accountService, + } +} + +// Logout from WhatsApp +// @Summary Logout from WhatsApp +// @Description Logs out from the specified WhatsApp instance. +// @Tags WhatsApp Logout +// @Param instanceId path string true "Instance ID" +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "Logout successful" +// @Router /{instanceId}/logout [post] +func (h *logoutHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if !h.whatsAppService.IsAuthenticated(instance) { + response.ErrorResponse(c, http.StatusUnauthorized, "unautenticated") + return + } + + err = h.whatsAppService.Logout(instance) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, gin.H{}) +} diff --git a/api/handler/send_audio_message_handler.go b/api/handler/send_audio_message_handler.go new file mode 100644 index 0000000..a148b87 --- /dev/null +++ b/api/handler/send_audio_message_handler.go @@ -0,0 +1,122 @@ +package handler + +import ( + "net/http" + "zapmeow/api/helper" + "zapmeow/api/model" + "zapmeow/api/response" + "zapmeow/api/service" + + "github.com/gin-gonic/gin" + "github.com/vincent-petithory/dataurl" +) + +type sendAudioMessageBody struct { + Phone string `json:"phone"` + Base64 string `json:"base64"` +} + +type sendAudioMessageResponse struct { + Message response.Message `json:"message"` +} + +type sendAudioMessageHandler struct { + whatsAppService service.WhatsAppService + messageService service.MessageService +} + +func NewSendAudioMessageHandler( + whatsAppService service.WhatsAppService, + messageService service.MessageService, +) *sendAudioMessageHandler { + return &sendAudioMessageHandler{ + whatsAppService: whatsAppService, + messageService: messageService, + } +} + +// Send Audio Message on WhatsApp +// @Summary Send Audio Message on WhatsApp +// @Description Sends an audio message on WhatsApp using the specified instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body sendAudioMessageBody true "Audio message body" +// @Accept json +// @Produce json +// @Success 200 {object} sendAudioMessageResponse "Message Send Response" +// @Router /{instanceId}/chat/send/audio [post] +func (h *sendAudioMessageHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if !h.whatsAppService.IsAuthenticated(instance) { + response.ErrorResponse(c, http.StatusUnauthorized, "unautenticated") + return + } + + var body sendAudioMessageBody + if err := c.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + return + } + + jid, ok := helper.MakeJID(body.Phone) + if !ok { + response.ErrorResponse(c, http.StatusBadRequest, "Invalid phone") + return + } + + mimitype, err := helper.GetMimeTypeFromDataURI(body.Base64) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + audioURL, err := dataurl.DecodeString(body.Base64) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + resp, err := h.whatsAppService.SendImageMessage(instance, jid, audioURL, mimitype) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + path, err := helper.SaveMedia( + instanceID, + resp.ID, + audioURL.Data, + mimitype, + ) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + message := model.Message{ + FromMe: true, + ChatJID: jid.User, + SenderJID: resp.Sender.User, + InstanceID: instanceID, + Timestamp: resp.Timestamp, + MessageID: resp.ID, + MediaType: "audio", + MediaPath: path, + } + + err = h.messageService.CreateMessage(&message) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, sendAudioMessageResponse{ + Message: response.NewMessageResponse(message), + }) +} diff --git a/api/handler/send_image_message_handler.go b/api/handler/send_image_message_handler.go new file mode 100644 index 0000000..30e048a --- /dev/null +++ b/api/handler/send_image_message_handler.go @@ -0,0 +1,122 @@ +package handler + +import ( + "net/http" + "zapmeow/api/helper" + "zapmeow/api/model" + "zapmeow/api/response" + "zapmeow/api/service" + + "github.com/gin-gonic/gin" + "github.com/vincent-petithory/dataurl" +) + +type sendImageMessageBody struct { + Phone string `json:"phone"` + Base64 string `json:"base64"` +} + +type sendImageMessageResponse struct { + Message response.Message `json:"message"` +} + +type sendImageMessageHandler struct { + whatsAppService service.WhatsAppService + messageService service.MessageService +} + +func NewSendImageMessageHandler( + whatsAppService service.WhatsAppService, + messageService service.MessageService, +) *sendImageMessageHandler { + return &sendImageMessageHandler{ + whatsAppService: whatsAppService, + messageService: messageService, + } +} + +// Send Image Message on WhatsApp +// @Summary Send Image Message on WhatsApp +// @Description Sends an image message on WhatsApp using the specified instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body sendImageMessageBody true "Image message body" +// @Accept json +// @Produce json +// @Success 200 {object} sendImageMessageResponse "Message Send Response" +// @Router /{instanceId}/chat/send/image [post] +func (h *sendImageMessageHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + if !h.whatsAppService.IsAuthenticated(instance) { + response.ErrorResponse(c, http.StatusUnauthorized, "unautenticated") + return + } + + var body sendImageMessageBody + if err := c.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + return + } + + jid, ok := helper.MakeJID(body.Phone) + if !ok { + response.ErrorResponse(c, http.StatusBadRequest, "Invalid phone") + return + } + + mimitype, err := helper.GetMimeTypeFromDataURI(body.Base64) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + imageURL, err := dataurl.DecodeString(body.Base64) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + resp, err := h.whatsAppService.SendImageMessage(instance, jid, imageURL, mimitype) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + path, err := helper.SaveMedia( + instanceID, + resp.ID, + imageURL.Data, + mimitype, + ) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + message := model.Message{ + FromMe: true, + ChatJID: jid.User, + SenderJID: resp.Sender.User, + InstanceID: instanceID, + Timestamp: resp.Timestamp, + MessageID: resp.ID, + MediaType: "image", + MediaPath: path, + } + + err = h.messageService.CreateMessage(&message) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, sendImageMessageResponse{ + Message: response.NewMessageResponse(message), + }) +} diff --git a/api/handler/send_text_message_handler.go b/api/handler/send_text_message_handler.go new file mode 100644 index 0000000..bd04c3d --- /dev/null +++ b/api/handler/send_text_message_handler.go @@ -0,0 +1,93 @@ +package handler + +import ( + "net/http" + "zapmeow/api/helper" + "zapmeow/api/model" + "zapmeow/api/response" + "zapmeow/api/service" + + "github.com/gin-gonic/gin" +) + +type sendTextMessageBody struct { + Phone string `json:"phone"` + Text string `json:"text"` +} + +type sendTextMessageResponse struct { + Message response.Message `json:"message"` +} + +type sendTextMessageHandler struct { + whatsAppService service.WhatsAppService + messageService service.MessageService +} + +func NewSendTextMessageHandler( + whatsAppService service.WhatsAppService, + messageService service.MessageService, +) *sendTextMessageHandler { + return &sendTextMessageHandler{ + whatsAppService: whatsAppService, + messageService: messageService, + } +} + +// Send Text Message on WhatsApp +// @Summary Send Text Message on WhatsApp +// @Description Sends a text message on WhatsApp using the specified instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body sendTextMessageBody true "Text message body" +// @Accept json +// @Produce json +// @Success 200 {object} sendTextMessageResponse "Message Send Response" +// @Router /{instanceId}/chat/send/text [post] +func (h *sendTextMessageHandler) Handler(c *gin.Context) { + instanceID := c.Param("instanceId") + instance, err := h.whatsAppService.GetInstance(instanceID) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + var body sendTextMessageBody + if err := c.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(c, http.StatusBadRequest, "Error trying to validate infos. ") + + return + } + + jid, ok := helper.MakeJID(body.Phone) + if !ok { + response.ErrorResponse(c, http.StatusBadRequest, "Invalid phone") + return + } + + resp, err := h.whatsAppService.SendTextMessage(instance, jid, body.Text) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + message := model.Message{ + MessageID: resp.ID, + ChatJID: jid.User, + SenderJID: resp.Sender.User, + InstanceID: instanceID, + Body: body.Text, + Timestamp: resp.Timestamp, + FromMe: true, + } + + err = h.messageService.CreateMessage(&message) + if err != nil { + response.ErrorResponse(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Response(c, http.StatusOK, sendTextMessageResponse{ + Message: response.NewMessageResponse(message), + }) +} diff --git a/utils/get_mime_type_from_data_uri.go b/api/helper/get_mime_type_from_data_uri.go similarity index 97% rename from utils/get_mime_type_from_data_uri.go rename to api/helper/get_mime_type_from_data_uri.go index b3ff702..e6a69ff 100644 --- a/utils/get_mime_type_from_data_uri.go +++ b/api/helper/get_mime_type_from_data_uri.go @@ -1,4 +1,4 @@ -package utils +package helper import ( "errors" diff --git a/utils/make_account_storage_path.go b/api/helper/make_account_storage_path.go similarity index 69% rename from utils/make_account_storage_path.go rename to api/helper/make_account_storage_path.go index 77e7734..050a39f 100644 --- a/utils/make_account_storage_path.go +++ b/api/helper/make_account_storage_path.go @@ -1,11 +1,11 @@ -package utils +package helper import ( "fmt" - "zapmeow/configs" + "zapmeow/config" ) func MakeAccountStoragePath(instanceID string) string { - config := configs.LoadConfigs() + config := config.Load() return fmt.Sprintf("%s/instance_%s", config.StoragePath, instanceID) } diff --git a/utils/make_jid.go b/api/helper/make_jid.go similarity index 98% rename from utils/make_jid.go rename to api/helper/make_jid.go index c94c7be..95e3c80 100644 --- a/utils/make_jid.go +++ b/api/helper/make_jid.go @@ -1,4 +1,4 @@ -package utils +package helper import ( "strings" diff --git a/utils/min.go b/api/helper/min.go similarity index 81% rename from utils/min.go rename to api/helper/min.go index 43d30e7..a2e5dad 100644 --- a/utils/min.go +++ b/api/helper/min.go @@ -1,4 +1,4 @@ -package utils +package helper func Min(a, b int) int { if a < b { diff --git a/utils/save_media.go b/api/helper/save_media.go similarity index 97% rename from utils/save_media.go rename to api/helper/save_media.go index d15284f..a822091 100644 --- a/utils/save_media.go +++ b/api/helper/save_media.go @@ -1,4 +1,4 @@ -package utils +package helper import ( "fmt" diff --git a/models/account.go b/api/model/account.go similarity index 93% rename from models/account.go rename to api/model/account.go index 2f77b40..613ff08 100644 --- a/models/account.go +++ b/api/model/account.go @@ -1,4 +1,4 @@ -package models +package model import "gorm.io/gorm" diff --git a/models/message.go b/api/model/message.go similarity index 95% rename from models/message.go rename to api/model/message.go index 3ea54ec..d4c722b 100644 --- a/models/message.go +++ b/api/model/message.go @@ -1,4 +1,4 @@ -package models +package model import ( "time" diff --git a/queues/history_sync_queue.go b/api/queue/history_sync_queue.go similarity index 52% rename from queues/history_sync_queue.go rename to api/queue/history_sync_queue.go index 14cbac5..ce74568 100644 --- a/queues/history_sync_queue.go +++ b/api/queue/history_sync_queue.go @@ -1,10 +1,9 @@ -package queues +package queue import ( "encoding/json" - "zapmeow/configs" - - "github.com/go-redis/redis" + "zapmeow/pkg/logger" + "zapmeow/pkg/zapmeow" ) type HistorySyncQueueData struct { @@ -13,9 +12,7 @@ type HistorySyncQueueData struct { } type historySyncQueue struct { - client *redis.Client - app *configs.ZapMeow - log configs.Logger + app *zapmeow.ZapMeow } type HistorySyncQueue interface { @@ -23,39 +20,42 @@ type HistorySyncQueue interface { Dequeue() (*HistorySyncQueueData, error) } -func NewHistorySyncQueue(app *configs.ZapMeow, log configs.Logger) *historySyncQueue { +func NewHistorySyncQueue(app *zapmeow.ZapMeow) *historySyncQueue { return &historySyncQueue{ app: app, - log: log, } } func (q *historySyncQueue) Enqueue(item HistorySyncQueueData) error { jsonData, err := json.Marshal(item) if err != nil { - q.log.Error("Error marshal history sync", err) + logger.Error("Error enqueue history sync.", logger.Fields{ + "error": err, + }) return err } - return q.app.RedisClient.LPush(q.app.Config.QueueName, jsonData).Err() + return q.app.Queue.Enqueue(q.app.Config.QueueName, jsonData) } func (q *historySyncQueue) Dequeue() (*HistorySyncQueueData, error) { - result, err := q.app.RedisClient.RPop(q.app.Config.QueueName).Bytes() + result, err := q.app.Queue.Dequeue(q.app.Config.QueueName) if err != nil { - if err == redis.Nil { - return nil, nil - } else { - q.log.Error("Error dequeuing history sync", err) - return nil, err - } + logger.Error("Error dequeuing history sync", logger.Fields{ + "error": err, + }) + return nil, err + } + if result == nil { + return nil, nil } var data HistorySyncQueueData err = json.Unmarshal(result, &data) - if err != nil { - q.log.Error("Error unmarshal history sync.", err) + logger.Error("Error unmarshal history sync.", logger.Fields{ + "error": err, + }) return nil, err } diff --git a/api/repository/account_repository.go b/api/repository/account_repository.go new file mode 100644 index 0000000..5b31472 --- /dev/null +++ b/api/repository/account_repository.go @@ -0,0 +1,58 @@ +package repository + +import ( + "zapmeow/api/model" + "zapmeow/pkg/database" + + "gorm.io/gorm" +) + +type AccountRepository interface { + CreateAccount(account *model.Account) error + GetConnectedAccounts() ([]model.Account, error) + GetAccountByInstanceID(instanceID string) (*model.Account, error) + UpdateAccount(instanceID string, data map[string]interface{}) error +} + +type accountRepository struct { + database database.Database +} + +func NewAccountRepository(database database.Database) *accountRepository { + return &accountRepository{database: database} +} + +func (repo *accountRepository) CreateAccount(account *model.Account) error { + return repo.database.Client().Create(account).Error +} + +func (repo *accountRepository) GetConnectedAccounts() ([]model.Account, error) { + var accounts []model.Account + repo.database.Client().Where("status = ?", "CONNECTED").Find(&accounts) + return accounts, nil +} + +func (repo *accountRepository) GetAccountByInstanceID(instanceID string) (*model.Account, error) { + var account model.Account + result := repo.database.Client().Where("instance_id = ?", instanceID).First(&account) + if result.Error != nil { + if result.Error != gorm.ErrRecordNotFound { + return nil, result.Error + } + return nil, nil + } + return &account, nil +} + +func (repo *accountRepository) UpdateAccount(instanceID string, data map[string]interface{}) error { + var account model.Account + if result := repo.database.Client().Where("instance_id = ?", instanceID).First(&account); result.Error != nil { + return result.Error + } + + if err := repo.database.Client().Model(&account).Updates(data).Error; err != nil { + return err + } + + return nil +} diff --git a/api/repository/message_repository.go b/api/repository/message_repository.go new file mode 100644 index 0000000..af47c61 --- /dev/null +++ b/api/repository/message_repository.go @@ -0,0 +1,53 @@ +package repository + +import ( + "zapmeow/api/model" + "zapmeow/pkg/database" +) + +type MessageRepository interface { + CreateMessage(message *model.Message) error + CreateMessages(messages *[]model.Message) error + GetChatMessages(instanceID string, chatJID string) (*[]model.Message, error) + CountChatMessages(instanceID string, chatJID string) (int64, error) + DeleteMessagesByInstanceID(instanceID string) error +} + +type messageRepository struct { + database database.Database +} + +func NewMessageRepository(database database.Database) *messageRepository { + return &messageRepository{database: database} +} + +func (repo *messageRepository) CreateMessage(message *model.Message) error { + return repo.database.Client().Create(message).Error +} + +func (repo *messageRepository) CreateMessages(messages *[]model.Message) error { + return repo.database.Client().Create(messages).Error +} + +func (repo *messageRepository) CountChatMessages(instanceID string, chatJID string) (int64, error) { + var count int64 + if result := repo.database.Client().Model(&model.Message{}).Where("instance_id = ? AND chat_jid = ?", instanceID, chatJID).Count(&count); result.Error != nil { + return 0, result.Error + } + return count, nil +} + +func (repo *messageRepository) GetChatMessages(instanceID string, chatJID string) (*[]model.Message, error) { + var messages []model.Message + if result := repo.database.Client().Where("instance_id = ? AND chat_jid = ?", instanceID, chatJID).Order("timestamp DESC").Find(&messages); result.Error != nil { + return nil, result.Error + } + return &messages, nil +} + +func (repo *messageRepository) DeleteMessagesByInstanceID(instanceID string) error { + if result := repo.database.Client().Where("instance_id = ?", instanceID).Unscoped().Delete(&model.Message{}); result.Error != nil { + return result.Error + } + return nil +} diff --git a/api/response/message_response.go b/api/response/message_response.go new file mode 100644 index 0000000..e435e10 --- /dev/null +++ b/api/response/message_response.go @@ -0,0 +1,59 @@ +package response + +import ( + "encoding/base64" + "mime" + "os" + "path/filepath" + "time" + "zapmeow/api/model" +) + +type Message struct { + ID uint `json:"id"` + Sender string `json:"sender"` + Chat string `json:"chat"` + MessageID string `json:"message_id"` + FromMe bool `json:"from_me"` + Timestamp time.Time `json:"timestamp"` + Body string `json:"body"` + MediaType string `json:"media_type"` + MediaMimeType string `json:"media_mimetype"` + MediaBase64 string `json:"media_base64"` +} + +func NewMessageResponse(msg model.Message) Message { + data := Message{ + ID: msg.ID, + Sender: msg.SenderJID, + Chat: msg.ChatJID, + MessageID: msg.MessageID, + FromMe: msg.FromMe, + Timestamp: msg.Timestamp, + Body: msg.Body, + MediaType: msg.MediaType, + } + + if msg.MediaType != "" { + media, err := os.ReadFile(msg.MediaPath) + if err != nil { + // logger.Error("Error reading the file. ", err) + } else { + mimetype := mime.TypeByExtension(filepath.Ext(msg.MediaPath)) + base64 := base64.StdEncoding.EncodeToString(media) + data.MediaMimeType = mimetype + data.MediaBase64 = base64 + } + } + + return data +} + +func NewMessagesResponse(msgs *[]model.Message) []Message { + var data []Message + for _, message := range *msgs { + data = append(data, NewMessageResponse(message)) + } + + return data +} diff --git a/api/response/response.go b/api/response/response.go new file mode 100644 index 0000000..1508a43 --- /dev/null +++ b/api/response/response.go @@ -0,0 +1,65 @@ +package response + +import "github.com/gin-gonic/gin" + +type Error struct { + Code int `json:"code"` + Error string `json:"error"` +} + +type Data struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func Response(c *gin.Context, statusCode int, data interface{}) { + c.JSON(statusCode, data) +} + +func MessageResponse(c *gin.Context, statusCode int, message string) { + Response(c, statusCode, Data{ + Code: statusCode, + Message: message, + }) +} + +func ErrorResponse(c *gin.Context, statusCode int, message string) { + Response(c, statusCode, Error{ + Code: statusCode, + Error: message, + }) + c.Abort() +} + +// func RespondWithSuccess(c *gin.Context, data interface{}) { +// c.JSON(http.StatusOK, gin.H{ +// "Success": true, +// "Data": data, +// }) +// } + +// func RespondWithError(c *gin.Context, statusCode int, message string) { +// c.JSON(statusCode, gin.H{ +// "Success": false, +// "Error": message, +// }) +// c.Abort() +// } + +// func RespondNotFound(c *gin.Context, message string) { +// RespondWithError(c, http.StatusNotFound, message) +// } + +// func RespondBadRequest(c *gin.Context, message string) { +// RespondWithError(c, http.StatusBadRequest, message) +// } + +// func RespondInternalServerError(c *gin.Context, message string) { +// RespondWithError(c, http.StatusInternalServerError, message) +// } + +// func HandleError(c *gin.Context, err error) { +// if err != nil { +// RespondInternalServerError(c, "Internal server error") +// } +// } diff --git a/api/route/routes.go b/api/route/routes.go new file mode 100644 index 0000000..41fc80f --- /dev/null +++ b/api/route/routes.go @@ -0,0 +1,90 @@ +package route + +import ( + "zapmeow/api/handler" + "zapmeow/api/service" + "zapmeow/config" + "zapmeow/pkg/zapmeow" + + docs "zapmeow/docs" + + "github.com/gin-gonic/gin" + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func makeEngine(app *zapmeow.ZapMeow) *gin.Engine { + if app.Config.Environment == config.Development { + return gin.New() + } + return gin.Default() +} + +func SetupRouter( + app *zapmeow.ZapMeow, + whatsAppService service.WhatsAppService, + messageService service.MessageService, + accountService service.AccountService, +) *gin.Engine { + docs.SwaggerInfo.BasePath = "/api" + + router := makeEngine(app) + + getQrCodeHandler := handler.NewGetQrCodeHandler( + app, + whatsAppService, + messageService, + accountService, + ) + logoutHandler := handler.NewLogoutHandler( + app, + whatsAppService, + accountService, + ) + getStatusHandler := handler.NewGetStatusHandler( + app, + whatsAppService, + accountService, + ) + getProfileInfoHandler := handler.NewGetProfileInfoHandler( + whatsAppService, + ) + getContactInfoHandler := handler.NewGetContactInfoHandler( + whatsAppService, + ) + checkPhonesHandler := handler.NewCheckPhonesHandler( + whatsAppService, + ) + getMessagesHandler := handler.NewGetMessagesHandler( + whatsAppService, + messageService, + ) + sendTextMessageHandler := handler.NewSendTextMessageHandler( + whatsAppService, + messageService, + ) + sendImageMessageHandler := handler.NewSendImageMessageHandler( + whatsAppService, + messageService, + ) + sendAudioMessageHandler := handler.NewSendAudioMessageHandler( + whatsAppService, + messageService, + ) + + group := router.Group("/api") + + group.GET("/:instanceId/qrcode", getQrCodeHandler.Handler) + group.GET("/:instanceId/status", getStatusHandler.Handler) + group.GET("/:instanceId/profile", getProfileInfoHandler.Handler) + group.GET("/:instanceId/contact/info", getContactInfoHandler.Handler) + group.POST("/:instanceId/logout", logoutHandler.Handler) + group.POST("/:instanceId/check/phones", checkPhonesHandler.Handler) + group.POST("/:instanceId/chat/messages", getMessagesHandler.Handler) + group.POST("/:instanceId/chat/send/text", sendTextMessageHandler.Handler) + group.POST("/:instanceId/chat/send/image", sendImageMessageHandler.Handler) + group.POST("/:instanceId/chat/send/audio", sendAudioMessageHandler.Handler) + group.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + + return router +} diff --git a/services/account_service.go b/api/service/account_service.go similarity index 66% rename from services/account_service.go rename to api/service/account_service.go index 8d6abbd..9d9986f 100644 --- a/services/account_service.go +++ b/api/service/account_service.go @@ -1,42 +1,42 @@ -package services +package service import ( "os" "path/filepath" - "zapmeow/models" - "zapmeow/repositories" - "zapmeow/utils" + "zapmeow/api/helper" + "zapmeow/api/model" + "zapmeow/api/repository" ) type AccountService interface { - CreateAccount(account *models.Account) error - GetConnectedAccounts() ([]models.Account, error) - GetAccountByInstanceID(instanceID string) (*models.Account, error) + CreateAccount(account *model.Account) error + GetConnectedAccounts() ([]model.Account, error) + GetAccountByInstanceID(instanceID string) (*model.Account, error) UpdateAccount(instanceID string, data map[string]interface{}) error DeleteAccountMessages(instanceID string) error } type accountService struct { - accountRepo repositories.AccountRepository + accountRepo repository.AccountRepository messageService MessageService } -func NewAccountService(accountRepo repositories.AccountRepository, messageService MessageService) *accountService { +func NewAccountService(accountRepo repository.AccountRepository, messageService MessageService) *accountService { return &accountService{ accountRepo: accountRepo, messageService: messageService, } } -func (a *accountService) CreateAccount(account *models.Account) error { +func (a *accountService) CreateAccount(account *model.Account) error { return a.accountRepo.CreateAccount(account) } -func (a *accountService) GetConnectedAccounts() ([]models.Account, error) { +func (a *accountService) GetConnectedAccounts() ([]model.Account, error) { return a.accountRepo.GetConnectedAccounts() } -func (a *accountService) GetAccountByInstanceID(instanceID string) (*models.Account, error) { +func (a *accountService) GetAccountByInstanceID(instanceID string) (*model.Account, error) { return a.accountRepo.GetAccountByInstanceID(instanceID) } @@ -53,7 +53,7 @@ func (a *accountService) DeleteAccountMessages(instanceID string) error { } func (a *accountService) deleteAccountDirectory(instanceID string) error { - dirPath := utils.MakeAccountStoragePath(instanceID) + dirPath := helper.MakeAccountStoragePath(instanceID) err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err diff --git a/api/service/account_service_test.go b/api/service/account_service_test.go new file mode 100644 index 0000000..1ccaf1d --- /dev/null +++ b/api/service/account_service_test.go @@ -0,0 +1 @@ +package service_test diff --git a/api/service/message_service.go b/api/service/message_service.go new file mode 100644 index 0000000..5822172 --- /dev/null +++ b/api/service/message_service.go @@ -0,0 +1,44 @@ +package service + +import ( + "zapmeow/api/model" + "zapmeow/api/repository" +) + +type MessageService interface { + CreateMessage(message *model.Message) error + CreateMessages(messages *[]model.Message) error + GetChatMessages(instanceID string, chatJID string) (*[]model.Message, error) + CountChatMessages(instanceID string, chatJID string) (int64, error) + DeleteMessagesByInstanceID(instanceID string) error +} + +type messageService struct { + messageRep repository.MessageRepository +} + +func NewMessageService(messageRep repository.MessageRepository) *messageService { + return &messageService{ + messageRep: messageRep, + } +} + +func (m *messageService) CreateMessage(message *model.Message) error { + return m.messageRep.CreateMessage(message) +} + +func (m *messageService) CreateMessages(messages *[]model.Message) error { + return m.messageRep.CreateMessages(messages) +} + +func (m *messageService) GetChatMessages(instanceID string, chatJID string) (*[]model.Message, error) { + return m.messageRep.GetChatMessages(instanceID, chatJID) +} + +func (m *messageService) CountChatMessages(instanceID string, chatJID string) (int64, error) { + return m.messageRep.CountChatMessages(instanceID, chatJID) +} + +func (m *messageService) DeleteMessagesByInstanceID(instanceID string) error { + return m.messageRep.DeleteMessagesByInstanceID(instanceID) +} diff --git a/api/service/message_service_test.go b/api/service/message_service_test.go new file mode 100644 index 0000000..1ccaf1d --- /dev/null +++ b/api/service/message_service_test.go @@ -0,0 +1 @@ +package service_test diff --git a/api/service/whatsapp_service.go b/api/service/whatsapp_service.go new file mode 100644 index 0000000..92b6f29 --- /dev/null +++ b/api/service/whatsapp_service.go @@ -0,0 +1,334 @@ +package service + +import ( + "zapmeow/api/helper" + "zapmeow/api/model" + "zapmeow/api/queue" + "zapmeow/api/response" + "zapmeow/pkg/http" + "zapmeow/pkg/logger" + "zapmeow/pkg/whatsapp" + "zapmeow/pkg/zapmeow" + + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" + "google.golang.org/protobuf/proto" +) + +type whatsAppService struct { + app *zapmeow.ZapMeow + messageService MessageService + accountService AccountService + whatsApp whatsapp.WhatsApp +} + +type WhatsAppService interface { + GetInstance(instanceID string) (*whatsapp.Instance, error) + IsAuthenticated(instance *whatsapp.Instance) bool + Logout(instance *whatsapp.Instance) error + SendTextMessage(instance *whatsapp.Instance, jid whatsapp.JID, text string) (whatsapp.MessageResponse, error) + SendAudioMessage(instance *whatsapp.Instance, jid whatsapp.JID, audioURL *dataurl.DataURL, mimitype string) (whatsapp.MessageResponse, error) + SendImageMessage(instance *whatsapp.Instance, jid whatsapp.JID, imageURL *dataurl.DataURL, mimitype string) (whatsapp.MessageResponse, error) + GetContactInfo(instance *whatsapp.Instance, jid whatsapp.JID) (*whatsapp.ContactInfo, error) + ParseEventMessage(instance *whatsapp.Instance, message *events.Message) (whatsapp.Message, error) + IsOnWhatsApp(instance *whatsapp.Instance, phones []string) ([]whatsapp.IsOnWhatsAppResponse, error) +} + +func NewWhatsAppService( + app *zapmeow.ZapMeow, + messageService MessageService, + accountService AccountService, + whatsApp whatsapp.WhatsApp, +) *whatsAppService { + return &whatsAppService{ + app: app, + messageService: messageService, + accountService: accountService, + whatsApp: whatsApp, + } +} + +func (w *whatsAppService) SendTextMessage( + instance *whatsapp.Instance, + jid whatsapp.JID, + text string, +) (whatsapp.MessageResponse, error) { + return w.whatsApp.SendTextMessage(instance, jid, text) +} + +func (w *whatsAppService) SendAudioMessage( + instance *whatsapp.Instance, + jid whatsapp.JID, + audioURL *dataurl.DataURL, + mimitype string, +) (whatsapp.MessageResponse, error) { + return w.whatsApp.SendAudioMessage(instance, jid, audioURL, mimitype) +} + +func (w *whatsAppService) SendImageMessage( + instance *whatsapp.Instance, + jid whatsapp.JID, + imageURL *dataurl.DataURL, + mimitype string, +) (whatsapp.MessageResponse, error) { + return w.whatsApp.SendImageMessage(instance, jid, imageURL, mimitype) +} + +func (w *whatsAppService) GetContactInfo(instance *whatsapp.Instance, jid whatsapp.JID) (*whatsapp.ContactInfo, error) { + return w.whatsApp.GetContactInfo(instance, jid) +} + +func (w *whatsAppService) ParseEventMessage(instance *whatsapp.Instance, message *events.Message) (whatsapp.Message, error) { + return w.whatsApp.ParseEventMessage(instance, message) +} + +func (w *whatsAppService) IsOnWhatsApp(instance *whatsapp.Instance, phones []string) ([]whatsapp.IsOnWhatsAppResponse, error) { + return w.whatsApp.IsOnWhatsApp(instance, phones) +} + +func (w *whatsAppService) GetInstance(instanceID string) (*whatsapp.Instance, error) { + instance := w.app.LoadInstance(instanceID) + if instance != nil { + return instance, nil + } + + instance, err := w.gerOrCreateInstance(instanceID) + if err != nil { + return nil, err + } + w.app.StoreInstance(instanceID, instance) + + instance = w.app.LoadInstance(instanceID) + instance.Client.AddEventHandler(func(evt interface{}) { + w.eventHandler(instanceID, evt) + }) + + err = w.whatsApp.InitInstance(instance, func(event string, code string, err error) { + switch event { + case "code": + { + instance.QrCodeRateLimit -= 1 + err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ + "QrCode": code, + "Status": "UNPAIRED", + "WasSynced": false, + }) + if err != nil { + logger.Error("Failed to update account. ", err) + } + } + case "error": + { + } + case "rate-limit": + { + err := w.deleteInstance(instance) + if err != nil { + logger.Info("a") + // logger.Error("Failed to destroy instance. ", err) + } + return + } + case "timeout": + { + err := w.accountService.UpdateAccount(instanceID, map[string]interface{}{ + "QrCode": "", + "Status": "TIMEOUT", + }) + if err != nil { + logger.Error("Failed to update account. ", err) + } + + w.deleteInstance(instance) + } + + } + }) + if err != nil { + return nil, err + } + + return instance, nil +} + +func (w *whatsAppService) IsAuthenticated(instance *whatsapp.Instance) bool { + return w.whatsApp.IsConnected(instance) && w.whatsApp.IsLoggedIn(instance) +} + +func (w *whatsAppService) Logout(instance *whatsapp.Instance) error { + err := w.whatsApp.Logout(instance) + if err != nil { + return err + } + + err = w.accountService.UpdateAccount(instance.ID, map[string]interface{}{ + "Status": "UNPAIRED", + }) + if err != nil { + return err + } + + return w.deleteInstance(instance) +} + +func (w *whatsAppService) gerOrCreateInstance(instanceID string) (*whatsapp.Instance, error) { + account, err := w.accountService.GetAccountByInstanceID(instanceID) + if err != nil { + return nil, err + } + + if account == nil || (account != nil && account.Status != "CONNECTED") { + instance := w.whatsApp.CreateInstance(instanceID) + + err := w.accountService.CreateAccount(&model.Account{ + InstanceID: instanceID, + }) + if err != nil { + return nil, err + } + return instance, nil + } + + jid := types.JID{ + User: account.User, + Agent: account.Agent, + Device: account.Device, + Server: account.Server, + AD: account.AD, + } + instance := w.whatsApp.CreateInstanceFromDevice( + instanceID, + jid, + ) + return instance, nil +} + +func (w *whatsAppService) deleteInstance(instance *whatsapp.Instance) error { + err := w.accountService.DeleteAccountMessages(instance.ID) + if err != nil { + return err + } + + w.whatsApp.Disconnect(instance) + w.app.DeleteInstance(instance.ID) + return nil +} + +func (w *whatsAppService) eventHandler(instanceID string, rawEvt interface{}) { + switch evt := rawEvt.(type) { + case *events.Message: + w.handleMessage(instanceID, evt) + case *events.HistorySync: + w.handleHistorySync(instanceID, evt) + case *events.Connected: + w.handleConnected(instanceID) + case *events.LoggedOut: + w.handleLoggedOut(instanceID) + } +} + +func (w *whatsAppService) handleHistorySync(instanceID string, evt *events.HistorySync) { + history, _ := proto.Marshal(evt.Data) + + q := queue.NewHistorySyncQueue(w.app) + err := q.Enqueue(queue.HistorySyncQueueData{ + History: history, + InstanceID: instanceID, + }) + + if err != nil { + logger.Error("Failed to add history sync to queue. ", err) + } +} + +func (w *whatsAppService) handleConnected(instanceID string) { + var instance = w.app.LoadInstance(instanceID) + err := w.accountService.UpdateAccount(instanceID, map[string]interface{}{ + "User": instance.Client.Store.ID.User, + "Agent": instance.Client.Store.ID.Agent, + "Device": instance.Client.Store.ID.Device, + "Server": instance.Client.Store.ID.Server, + "AD": instance.Client.Store.ID.AD, + "InstanceID": instance.ID, + "Status": "CONNECTED", + "QrCode": "", + "WasSynced": false, + }) + + if err != nil { + logger.Error("Failed to update account. ", err) + } +} + +func (w *whatsAppService) handleLoggedOut(instanceID string) { + instance, err := w.GetInstance(instanceID) + if err != nil { + logger.Error(err) + return + } + + err = w.deleteInstance(instance) + if err != nil { + logger.Error(err) + } + + err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ + "Status": "UNPAIRED", + }) + if err != nil { + logger.Error("Failed to update account. ", err) + } +} + +func (w *whatsAppService) handleMessage(instanceId string, evt *events.Message) { + instance := w.app.LoadInstance(instanceId) + parsedEventMessage, err := w.whatsApp.ParseEventMessage(instance, evt) + + if err != nil { + logger.Error(err) + return + } + + message := model.Message{ + SenderJID: parsedEventMessage.SenderJID, + ChatJID: parsedEventMessage.ChatJID, + InstanceID: parsedEventMessage.InstanceID, + MessageID: parsedEventMessage.MessageID, + Timestamp: parsedEventMessage.Timestamp, + Body: parsedEventMessage.Body, + FromMe: parsedEventMessage.FromMe, + } + + if parsedEventMessage.MediaType != nil { + path, err := helper.SaveMedia( + instance.ID, + parsedEventMessage.MessageID, + *parsedEventMessage.Media, + *parsedEventMessage.Mimetype, + ) + + if err != nil { + logger.Error("Failed to save media. ", err) + } + + message.MediaType = parsedEventMessage.MediaType.String() + message.MediaPath = path + } + + err = w.messageService.CreateMessage(&message) + if err != nil { + logger.Error("Failed to create message. ", err) + return + } + + body := map[string]interface{}{ + "InstanceId": instanceId, + "Message": response.NewMessageResponse(message), + } + + err = http.Request(w.app.Config.WebhookURL, body) + if err != nil { + logger.Error("Failed to send webhook request. ", err) + } +} diff --git a/api/service/whatsapp_service_test.go b/api/service/whatsapp_service_test.go new file mode 100644 index 0000000..d4da1f7 --- /dev/null +++ b/api/service/whatsapp_service_test.go @@ -0,0 +1,14 @@ +package service_test + +import ( + "testing" +) + +type MockWhatsapp struct{} + +type MockMessageService struct{} + +type MockAccountService struct{} + +func TestSendTextMessage(t *testing.T) { +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..75dad7e --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "sync" + "zapmeow/api/model" + "zapmeow/api/repository" + "zapmeow/api/route" + "zapmeow/api/service" + "zapmeow/config" + "zapmeow/pkg/database" + "zapmeow/pkg/logger" + "zapmeow/pkg/queue" + "zapmeow/pkg/whatsapp" + "zapmeow/pkg/zapmeow" + "zapmeow/worker" + + "github.com/joho/godotenv" +) + +// @title ZapMeow API +// @version 1.0 +// @description API to handle multiple WhatsApp instances +// @host localhost:8900 +// @BasePath /api +func main() { + logger.Init() + + err := godotenv.Load() + if err != nil { + logger.Fatal("Error loading dotfile. ", err) + } + cfg := config.Load() + + var instances sync.Map // whatsmeow instances + var mutex sync.Mutex + var wg sync.WaitGroup + wg.Add(1) + stopCh := make(chan struct{}) + + whatsApp := whatsapp.NewWhatsApp(cfg.DatabaseURL) + queue := queue.NewQueue(cfg.RedisAddr, cfg.RedisPassword) + + database := database.NewDatabase(cfg.DatabaseURL) + err = database.RunMigrate( + &model.Account{}, + &model.Message{}, + ) + if err != nil { + logger.Fatal("Error when running gorm automigrate. ", err) + } + + app := zapmeow.NewZapMeow( + database, + queue, + cfg, + &instances, + &wg, + &mutex, + &stopCh, + ) + + // repository + messageRepo := repository.NewMessageRepository(app.Database) + accountRepo := repository.NewAccountRepository(app.Database) + + // service + messageService := service.NewMessageService(messageRepo) + accountService := service.NewAccountService(accountRepo, messageService) + whatsAppService := service.NewWhatsAppService( + app, + messageService, + accountService, + whatsApp, + ) + + // workers + historySyncWorker := worker.NewHistorySyncWorker( + app, + messageService, + accountService, + whatsAppService, + ) + + r := route.SetupRouter( + app, + whatsAppService, + messageService, + accountService, + ) + + logger.Info("Loading whatsapp instances") + accounts, err := accountService.GetConnectedAccounts() + if err != nil { + logger.Fatal("Error getting accounts. ", err) + } + + for _, account := range accounts { + logger.Info("Loading instance: ", account.InstanceID) + _, err := whatsAppService.GetInstance(account.InstanceID) + if err != nil { + logger.Error("Error getting instance. ", err) + } + } + + go func() { + if err := r.Run(cfg.Port); err != nil { + logger.Fatal(err) + } + }() + + go historySyncWorker.ProcessQueue() + + <-*app.StopCh + app.Wg.Wait() + close(*app.StopCh) +} diff --git a/configs/config.go b/config/config.go similarity index 87% rename from configs/config.go rename to config/config.go index 475ebae..42ce680 100644 --- a/configs/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package configs +package config import "os" @@ -9,7 +9,7 @@ const ( Production ) -type ZapMeowConfig struct { +type Config struct { Environment Environment StoragePath string WebhookURL string @@ -21,7 +21,7 @@ type ZapMeowConfig struct { MaxMessagesPerInstance int } -func LoadConfigs() ZapMeowConfig { +func Load() Config { storagePath := os.Getenv("STORAGE_PATH") webhookURL := os.Getenv("WEBHOOK_URL") databaseURL := os.Getenv("DATABASE_PATH") @@ -30,7 +30,7 @@ func LoadConfigs() ZapMeowConfig { port := os.Getenv("PORT") env := getEnvironment() - return ZapMeowConfig{ + return Config{ Environment: env, StoragePath: storagePath, WebhookURL: webhookURL, @@ -38,7 +38,7 @@ func LoadConfigs() ZapMeowConfig { RedisAddr: redisAddr, RedisPassword: redisPassword, Port: port, - QueueName: "HISTORY_SYNC_QUEUE", + QueueName: "queue:history-sync", MaxMessagesPerInstance: 10, } } diff --git a/configs/app.go b/configs/app.go deleted file mode 100644 index 6f33de0..0000000 --- a/configs/app.go +++ /dev/null @@ -1,65 +0,0 @@ -package configs - -import ( - "sync" - - "github.com/go-redis/redis" - "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/store/sqlstore" - "gorm.io/gorm" -) - -type Instance struct { - ID string - Client *whatsmeow.Client - QrCodeRateLimit uint16 -} - -type ZapMeow struct { - WhatsmeowContainer *sqlstore.Container - DatabaseClient *gorm.DB - RedisClient *redis.Client - Instances *sync.Map - Config ZapMeowConfig - Wg *sync.WaitGroup - Mutex *sync.Mutex - StopCh *chan struct{} -} - -func NewZapMeow( - whatsmeowContainer *sqlstore.Container, - databaseClient *gorm.DB, - redisClient *redis.Client, - instances *sync.Map, - config ZapMeowConfig, - wg *sync.WaitGroup, - mutex *sync.Mutex, - stopCh *chan struct{}, -) *ZapMeow { - return &ZapMeow{ - WhatsmeowContainer: whatsmeowContainer, - DatabaseClient: databaseClient, - RedisClient: redisClient, - Instances: instances, - Config: config, - Wg: wg, - Mutex: mutex, - StopCh: stopCh, - } -} - -func (a *ZapMeow) LoadInstance(instanceID string) *Instance { - value, _ := a.Instances.Load(instanceID) - if value != nil { - return value.(*Instance) - } - return nil -} - -func (a *ZapMeow) StoreInstance(instanceID string, instance *Instance) { - a.Instances.Store(instanceID, instance) -} - -func (a *ZapMeow) DeleteInstance(instanceID string) { - a.Instances.Delete(instanceID) -} diff --git a/configs/logger.go b/configs/logger.go deleted file mode 100644 index 5ae0847..0000000 --- a/configs/logger.go +++ /dev/null @@ -1,59 +0,0 @@ -package configs - -import ( - "os" - - "github.com/sirupsen/logrus" -) - -type Logger interface { - Trace(args ...interface{}) - Debug(args ...interface{}) - Info(args ...interface{}) - Warn(args ...interface{}) - Error(args ...interface{}) - // Calls os.Exit(1) after logging - Fatal(args ...interface{}) - // Calls panic() after logging - Panic(args ...interface{}) -} - -type logger struct { - log *logrus.Logger -} - -func NewLogger() *logger { - log := logrus.New() - log.SetFormatter(&logrus.TextFormatter{ - DisableColors: true, - FullTimestamp: true, - }) - log.SetOutput(os.Stdout) - return &logger{ - log: log, - } -} - -func (l *logger) Trace(args ...interface{}) { - l.log.Trace(args...) -} -func (l *logger) Debug(args ...interface{}) { - l.log.Debug(args...) -} -func (l *logger) Info(args ...interface{}) { - l.log.Info(args...) -} -func (l *logger) Warn(args ...interface{}) { - l.log.Warn(args...) -} -func (l *logger) Error(args ...interface{}) { - l.log.Error(args...) -} - -func (l *logger) Fatal(args ...interface{}) { - l.log.Fatal(args...) -} - -func (l *logger) Panic(args ...interface{}) { - l.log.Panic(args...) -} diff --git a/controllers/check_phones_controller.go b/controllers/check_phones_controller.go deleted file mode 100644 index aaf1524..0000000 --- a/controllers/check_phones_controller.go +++ /dev/null @@ -1,96 +0,0 @@ -package controllers - -import ( - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type phoneCheckBody struct { - Phones []string -} - -type phone struct { - Query string - IsRegistered bool - JID struct { - AD bool - User string - Agent uint8 - Device uint8 - Server string - } -} - -type checkPhonesResponse struct { - Phones []phone -} - -type checkPhonesController struct { - wppService services.WppService -} - -func NewCheckPhonesController( - wppService services.WppService, -) *checkPhonesController { - return &checkPhonesController{ - wppService: wppService, - } -} - -// Check Phones on WhatsApp -// @Summary Check Phones on WhatsApp -// @Description Verifies if the phone numbers in the provided list are registered WhatsApp users. -// @Tags WhatsApp Phone Verification -// @Param instanceId path string true "Instance ID" -// @Param data body phoneCheckBody true "Phone list" -// @Accept json -// @Produce json -// @Success 200 {object} checkPhonesResponse "List of verified numbers" -// @Router /{instanceId}/check/phones [post] -func (p *checkPhonesController) Handler(c *gin.Context) { - var body phoneCheckBody - if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "Error trying to validate infos. ") - return - } - instanceID := c.Param("instanceId") - - instance, err := p.wppService.GetAuthenticatedInstance(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - phones, err := instance.Client.IsOnWhatsApp(body.Phones) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - data := make([]phone, len(phones)) - for i, p := range phones { - data[i] = phone{ - Query: p.Query, - IsRegistered: p.IsIn, - JID: struct { - AD bool - User string - Agent uint8 - Device uint8 - Server string - }{ - AD: p.JID.AD, - User: p.JID.User, - Agent: p.JID.Agent, - Device: p.JID.Device, - Server: p.JID.Server, - }, - } - } - - utils.RespondWithSuccess(c, checkPhonesResponse{ - Phones: data, - }) -} diff --git a/controllers/get_contract_info_controller.go b/controllers/get_contract_info_controller.go deleted file mode 100644 index 85834e2..0000000 --- a/controllers/get_contract_info_controller.go +++ /dev/null @@ -1,55 +0,0 @@ -package controllers - -import ( - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type getContactInfoController struct { - wppService services.WppService -} - -type contactInfoResponse struct { - Info services.ContactInfo -} - -func NewGetContactInfoController( - wppService services.WppService, -) *getContactInfoController { - return &getContactInfoController{ - wppService: wppService, - } -} - -// Get Contact Information -// @Summary Get Contact Information -// @Description Retrieves contact information. -// @Tags WhatsApp Contact -// @Param instanceId path string true "Instance ID" -// @Param phone query string true "Phone" -// @Accept json -// @Produce json -// @Success 200 {object} contactInfoResponse "Contact Information" -// @Router /{instanceId}/contact/info [get] -func (s *getContactInfoController) Handler(c *gin.Context) { - instanceID := c.Param("instanceId") - phone := c.Query("phone") - - jid, ok := utils.MakeJID(phone) - if !ok { - utils.RespondBadRequest(c, "Invalid phone") - return - } - - info, err := s.wppService.GetContactInfo(instanceID, jid) - if err != nil || info == nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - utils.RespondWithSuccess(c, contactInfoResponse{ - Info: *info, - }) -} diff --git a/controllers/get_messages_controller.go b/controllers/get_messages_controller.go deleted file mode 100644 index 369749c..0000000 --- a/controllers/get_messages_controller.go +++ /dev/null @@ -1,73 +0,0 @@ -package controllers - -import ( - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type getMessagesController struct { - wppService services.WppService - messageService services.MessageService -} - -type getMessagesResponse struct { - Messages []services.Message -} - -func NewGetMessagesController( - wppService services.WppService, - messageService services.MessageService, -) *getMessagesController { - return &getMessagesController{ - wppService: wppService, - messageService: messageService, - } -} - -// Get WhatsApp Chat Messages -// @Summary Get WhatsApp Chat Messages -// @Description Returns chat messages from the specified WhatsApp instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} getMessagesResponse "List of chat messages" -// @Router /{instanceId}/chat/messages [post] -func (m *getMessagesController) Handler(c *gin.Context) { - type Body struct { - Phone string - } - - var body Body - if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "error trying to validate infos") - return - } - instanceID := c.Param("instanceId") - - _, err := m.wppService.GetAuthenticatedInstance(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - messages, err := m.messageService.GetChatMessages( - instanceID, - body.Phone, - ) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - var data []services.Message - for _, message := range *messages { - data = append(data, m.messageService.ToJSON(message)) - } - - utils.RespondWithSuccess(c, getMessagesResponse{ - Messages: data, - }) -} diff --git a/controllers/get_profile_info_controller.go b/controllers/get_profile_info_controller.go deleted file mode 100644 index 252a373..0000000 --- a/controllers/get_profile_info_controller.go +++ /dev/null @@ -1,59 +0,0 @@ -package controllers - -import ( - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type getProfileInfoController struct { - wppService services.WppService -} - -type getProfileInfoResponse struct { - Info services.ContactInfo -} - -func NewGetProfileInfoController( - wppService services.WppService, -) *getProfileInfoController { - return &getProfileInfoController{ - wppService: wppService, - } -} - -// Get Profile Information -// @Summary Get Profile Information -// @Description Retrieves profile information. -// @Tags WhatsApp Profile -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} getProfileInfoResponse "Profile Information" -// @Router /{instanceId}/profile [get] -func (s *getProfileInfoController) Handler(c *gin.Context) { - instanceID := c.Param("instanceId") - - instance, err := s.wppService.GetAuthenticatedInstance(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - jid, ok := utils.MakeJID(instance.Client.Store.ID.User) - if !ok { - utils.RespondBadRequest(c, "Invalid phone") - return - } - - info, err := s.wppService.GetContactInfo(instanceID, jid) - if err != nil || info == nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - utils.RespondWithSuccess(c, getProfileInfoResponse{ - Info: *info, - }) -} diff --git a/controllers/get_qrcode_controller.go b/controllers/get_qrcode_controller.go deleted file mode 100644 index 0af281a..0000000 --- a/controllers/get_qrcode_controller.go +++ /dev/null @@ -1,69 +0,0 @@ -package controllers - -import ( - "zapmeow/configs" - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type getQrCodeController struct { - app *configs.ZapMeow - wppService services.WppService - messageService services.MessageService - accountService services.AccountService -} - -type getQrCodeResponse struct { - QrCode string -} - -func NewGetQrCodeController( - app *configs.ZapMeow, - wppService services.WppService, - messageService services.MessageService, - accountService services.AccountService, -) *getQrCodeController { - return &getQrCodeController{ - app: app, - wppService: wppService, - messageService: messageService, - accountService: accountService, - } -} - -// Get QR Code for WhatsApp Login -// @Summary Get WhatsApp QR Code -// @Description Returns a QR code to initiate WhatsApp login. -// @Tags WhatsApp Login -// @Param instanceId path string true "Instance ID" -// @Produce json -// @Success 200 {object} getQrCodeResponse "QR Code" -// @Router /{instanceId}/qrcode [get] -func (q *getQrCodeController) Handler(c *gin.Context) { - instanceID := c.Param("instanceId") - - _, err := q.wppService.GetInstance(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - q.app.Mutex.Lock() - defer q.app.Mutex.Unlock() - account, err := q.accountService.GetAccountByInstanceID(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - if account == nil { - utils.RespondNotFound(c, "Account not found") - return - } - - utils.RespondWithSuccess(c, getQrCodeResponse{ - QrCode: account.QrCode, - }) -} diff --git a/controllers/get_status_controller.go b/controllers/get_status_controller.go deleted file mode 100644 index a259e89..0000000 --- a/controllers/get_status_controller.go +++ /dev/null @@ -1,76 +0,0 @@ -package controllers - -import ( - "zapmeow/configs" - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type getStatusController struct { - app *configs.ZapMeow - wppService services.WppService - accountService services.AccountService -} - -type getStatusResponse struct { - Status string -} - -func NewGetStatusController( - app *configs.ZapMeow, - wppService services.WppService, - accountService services.AccountService, -) *getStatusController { - return &getStatusController{ - app: app, - wppService: wppService, - accountService: accountService, - } -} - -// Get WhatsApp Instance Status -// @Summary Get WhatsApp Instance Status -// @Description Returns the status of the specified WhatsApp instance. -// @Tags WhatsApp Status -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} getStatusResponse "Status Response" -// @Router /{instanceId}/status [get] -func (s *getStatusController) Handler(c *gin.Context) { - instanceID := c.Param("instanceId") - - instance, err := s.wppService.GetInstance(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - s.app.Mutex.Lock() - defer s.app.Mutex.Unlock() - account, err := s.accountService.GetAccountByInstanceID(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - if err != nil { - utils.RespondNotFound(c, "Account not found") - return - } - - var status = account.Status - if !instance.Client.IsConnected() { - status = "DISCONNECTED" - } - - if status == "CONNECTED" && !instance.Client.IsLoggedIn() { - status = "UNPAIRED" - } - - utils.RespondWithSuccess(c, getStatusResponse{ - Status: status, - }) -} diff --git a/controllers/logout_controller.go b/controllers/logout_controller.go deleted file mode 100644 index 0b5be3d..0000000 --- a/controllers/logout_controller.go +++ /dev/null @@ -1,47 +0,0 @@ -package controllers - -import ( - "zapmeow/configs" - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type logoutController struct { - app *configs.ZapMeow - wppService services.WppService - accountService services.AccountService -} - -func NewLogoutController( - app *configs.ZapMeow, - wppService services.WppService, - accountService services.AccountService, -) *logoutController { - return &logoutController{ - app: app, - wppService: wppService, - accountService: accountService, - } -} - -// Logout from WhatsApp -// @Summary Logout from WhatsApp -// @Description Logs out from the specified WhatsApp instance. -// @Tags WhatsApp Logout -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} map[string]interface{} "Logout successful" -// @Router /{instanceId}/logout [post] -func (s *logoutController) Handler(c *gin.Context) { - instanceID := c.Param("instanceId") - - err := s.wppService.Logout(instanceID) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - utils.RespondWithSuccess(c, gin.H{}) -} diff --git a/controllers/send_audio_message_controller.go b/controllers/send_audio_message_controller.go deleted file mode 100644 index 4264bd5..0000000 --- a/controllers/send_audio_message_controller.go +++ /dev/null @@ -1,109 +0,0 @@ -package controllers - -import ( - "zapmeow/models" - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" - "github.com/vincent-petithory/dataurl" -) - -type audioMessageBody struct { - Phone string - Base64 string -} - -type sendAudioMessageController struct { - wppService services.WppService - messageService services.MessageService -} - -type sendAudioMessageResponse struct { - Message services.Message -} - -func NewSendAudioMessageController( - wppService services.WppService, - messageService services.MessageService, -) *sendAudioMessageController { - return &sendAudioMessageController{ - wppService: wppService, - messageService: messageService, - } -} - -// Send Audio Message on WhatsApp -// @Summary Send Audio Message on WhatsApp -// @Description Sends an audio message on WhatsApp using the specified instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body audioMessageBody true "Audio message body" -// @Accept json -// @Produce json -// @Success 200 {object} sendAudioMessageResponse "Message Send Response" -// @Router /{instanceId}/chat/send/audio [post] -func (a *sendAudioMessageController) Handler(c *gin.Context) { - var body audioMessageBody - if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "error trying to validate infos") - return - } - - jid, ok := utils.MakeJID(body.Phone) - if !ok { - utils.RespondBadRequest(c, "Invalid phone") - return - } - instanceID := c.Param("instanceId") - - mimitype, err := utils.GetMimeTypeFromDataURI(body.Base64) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - audioURL, err := dataurl.DecodeString(body.Base64) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - resp, err := a.wppService.SendImageMessage(instanceID, jid, audioURL, mimitype) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - path, err := utils.SaveMedia( - instanceID, - resp.ID, - audioURL.Data, - mimitype, - ) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - message := models.Message{ - FromMe: true, - ChatJID: jid.User, - SenderJID: resp.Sender.User, - InstanceID: instanceID, - Timestamp: resp.Timestamp, - MessageID: resp.ID, - MediaType: "audio", - MediaPath: path, - } - - err = a.messageService.CreateMessage(&message) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - utils.RespondWithSuccess(c, sendAudioMessageResponse{ - Message: a.messageService.ToJSON(message), - }) -} diff --git a/controllers/send_image_message_controller.go b/controllers/send_image_message_controller.go deleted file mode 100644 index d52578b..0000000 --- a/controllers/send_image_message_controller.go +++ /dev/null @@ -1,109 +0,0 @@ -package controllers - -import ( - "zapmeow/models" - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" - "github.com/vincent-petithory/dataurl" -) - -type imageMessageBody struct { - Phone string - Base64 string -} - -type sendImageMessageController struct { - wppService services.WppService - messageService services.MessageService -} - -type sendImageMessageResponse struct { - Message services.Message -} - -func NewSendImageMessageController( - wppService services.WppService, - messageService services.MessageService, -) *sendImageMessageController { - return &sendImageMessageController{ - wppService: wppService, - messageService: messageService, - } -} - -// Send Image Message on WhatsApp -// @Summary Send Image Message on WhatsApp -// @Description Sends an image message on WhatsApp using the specified instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body imageMessageBody true "Image message body" -// @Accept json -// @Produce json -// @Success 200 {object} sendImageMessageResponse "Message Send Response" -// @Router /{instanceId}/chat/send/image [post] -func (i *sendImageMessageController) Handler(c *gin.Context) { - var body imageMessageBody - if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "error trying to validate infos") - return - } - - jid, ok := utils.MakeJID(body.Phone) - if !ok { - utils.RespondBadRequest(c, "Invalid phone") - return - } - instanceID := c.Param("instanceId") - - mimitype, err := utils.GetMimeTypeFromDataURI(body.Base64) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - imageURL, err := dataurl.DecodeString(body.Base64) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - resp, err := i.wppService.SendImageMessage(instanceID, jid, imageURL, mimitype) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - path, err := utils.SaveMedia( - instanceID, - resp.ID, - imageURL.Data, - mimitype, - ) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - message := models.Message{ - FromMe: true, - ChatJID: jid.User, - SenderJID: resp.Sender.User, - InstanceID: instanceID, - Timestamp: resp.Timestamp, - MessageID: resp.ID, - MediaType: "image", - MediaPath: path, - } - - err = i.messageService.CreateMessage(&message) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - utils.RespondWithSuccess(c, sendImageMessageResponse{ - Message: i.messageService.ToJSON(message), - }) -} diff --git a/controllers/send_text_message_controller.go b/controllers/send_text_message_controller.go deleted file mode 100644 index 05ad204..0000000 --- a/controllers/send_text_message_controller.go +++ /dev/null @@ -1,84 +0,0 @@ -package controllers - -import ( - "zapmeow/models" - "zapmeow/services" - "zapmeow/utils" - - "github.com/gin-gonic/gin" -) - -type textMessageBody struct { - Phone string - Text string -} - -type sendTextMessageController struct { - wppService services.WppService - messageService services.MessageService -} - -type sendTextMessageResponse struct { - Message services.Message -} - -func NewSendTextMessageController( - wppService services.WppService, - messageService services.MessageService, -) *sendTextMessageController { - return &sendTextMessageController{ - wppService: wppService, - messageService: messageService, - } -} - -// Send Text Message on WhatsApp -// @Summary Send Text Message on WhatsApp -// @Description Sends a text message on WhatsApp using the specified instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body textMessageBody true "Text message body" -// @Accept json -// @Produce json -// @Success 200 {object} sendTextMessageResponse "Message Send Response" -// @Router /{instanceId}/chat/send/text [post] -func (t *sendTextMessageController) Handler(c *gin.Context) { - var body textMessageBody - if err := c.ShouldBindJSON(&body); err != nil { - utils.RespondBadRequest(c, "error trying to validate infos") - return - } - - jid, ok := utils.MakeJID(body.Phone) - if !ok { - utils.RespondBadRequest(c, "Invalid phone") - return - } - instanceID := c.Param("instanceId") - - resp, err := t.wppService.SendTextMessage(instanceID, jid, body.Text) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - message := models.Message{ - ChatJID: jid.User, - SenderJID: resp.Sender.User, - InstanceID: instanceID, - Body: body.Text, - Timestamp: resp.Timestamp, - FromMe: true, - MessageID: resp.ID, - } - - err = t.messageService.CreateMessage(&message) - if err != nil { - utils.RespondInternalServerError(c, err.Error()) - return - } - - utils.RespondWithSuccess(c, sendTextMessageResponse{ - Message: t.messageService.ToJSON(message), - }) -} diff --git a/main.go b/main.go deleted file mode 100644 index 8d502da..0000000 --- a/main.go +++ /dev/null @@ -1,150 +0,0 @@ -package main - -import ( - "sync" - "zapmeow/configs" - "zapmeow/models" - "zapmeow/repositories" - "zapmeow/routes" - "zapmeow/services" - "zapmeow/workers" - - "github.com/go-redis/redis" - "github.com/joho/godotenv" - "go.mau.fi/whatsmeow/store/sqlstore" - waLog "go.mau.fi/whatsmeow/util/log" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -// @title ZapMeow API -// @version 1.0 -// @description API to handle multiple WhatsApp instances -// @host localhost:8900 -// @BasePath /api -func main() { - log := configs.NewLogger() - - err := godotenv.Load() - if err != nil { - log.Fatal("Error loading dotfile. ", err) - } - config := configs.LoadConfigs() - - // whatsmeow instances - var instances sync.Map - - // whatsmeow configs - dbLog := waLog.Stdout("Database", "DEBUG", true) - whatsmeowContainer, err := sqlstore.New("sqlite3", "file:"+config.DatabaseURL+"?_foreign_keys=on", dbLog) - if err != nil { - log.Fatal("Error loading sqlite whatsmeow container. ", err) - } - - databaseClient, err := gorm.Open(sqlite.Open(config.DatabaseURL), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - if err != nil { - log.Fatal("Error creating gorm database. ", err) - } - - db, err := databaseClient.DB() - if err != nil { - log.Fatal("Error getting gorm database. ", err) - } - defer db.Close() - - err = databaseClient.AutoMigrate( - &models.Account{}, - &models.Message{}, - ) - if err != nil { - log.Fatal("Error when running gorm automigrate. ", err) - } - - // redis configs - redisClient := redis.NewClient(&redis.Options{ - Addr: config.RedisAddr, - Password: config.RedisPassword, - DB: 0, - }) - - if _, err := redisClient.Ping().Result(); err != nil { - log.Fatal("Error when pinging redis. ", err) - } - - var mutex sync.Mutex - var wg sync.WaitGroup - wg.Add(1) - stopCh := make(chan struct{}) - - // app configs - app := configs.NewZapMeow( - whatsmeowContainer, - databaseClient, - redisClient, - &instances, - config, - &wg, - &mutex, - &stopCh, - ) - - // repositories - messageRepo := repositories.NewMessageRepository(app.DatabaseClient) - accountRepo := repositories.NewAccountRepository(app.DatabaseClient) - - // services - messageService := services.NewMessageService(messageRepo, log) - accountService := services.NewAccountService(accountRepo, messageService) - wppService := services.NewWppService( - app, - messageService, - accountService, - log, - ) - - // workers - historySyncWorker := workers.NewHistorySyncWorker( - app, - messageService, - accountService, - wppService, - log, - ) - - r := routes.SetupRouter( - app, - wppService, - messageService, - accountService, - ) - - log.Info("Loading whatsapp instances") - accounts, err := accountService.GetConnectedAccounts() - if err != nil { - log.Fatal("Error getting accounts. ", err) - } - - for _, account := range accounts { - log.Info("Loading instance: ", account.InstanceID) - _, err := wppService.GetInstance(account.InstanceID) - if err != nil { - log.Error("Error getting instance. ", err) - } - } - - go func() { - if err := r.Run(config.Port); err != nil { - log.Fatal(err) - } - }() - - go historySyncWorker.ProcessQueue() - - <-*app.StopCh - - app.Wg.Wait() - close(*app.StopCh) -} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..0c45947 --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,39 @@ +package database + +import ( + "zapmeow/pkg/logger" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" +) + +type Database interface { + RunMigrate(dst ...interface{}) error + Client() *gorm.DB +} + +type database struct { + client *gorm.DB +} + +func NewDatabase(databasePath string) *database { + client, err := gorm.Open(sqlite.Open(databasePath), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + if err != nil { + logger.Fatal("Error creating gorm database. ", err) + } + + return &database{ + client: client, + } +} + +func (d *database) RunMigrate(dst ...interface{}) error { + return d.client.AutoMigrate(dst...) +} + +func (d *database) Client() *gorm.DB { + return d.client +} diff --git a/utils/request.go b/pkg/http/http.go similarity index 83% rename from utils/request.go rename to pkg/http/http.go index 9388d37..b17e5c3 100644 --- a/utils/request.go +++ b/pkg/http/http.go @@ -1,14 +1,14 @@ -package utils +package http import ( "bytes" "encoding/json" + "errors" "net/http" ) func Request(url string, data map[string]interface{}) error { body, err := json.Marshal(data) - if err != nil { return err } @@ -22,7 +22,7 @@ func Request(url string, data map[string]interface{}) error { client := &http.Client{} resp, err := client.Do(req) if err != nil { - return err + return errors.New("Request returned an unexpected status code") } defer resp.Body.Close() diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..ae62d37 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,62 @@ +package logger + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +type Fields = logrus.Fields + +var log *logrus.Logger + +func Init() { + log = logrus.New() + + log.SetFormatter(&logrus.TextFormatter{ + DisableColors: true, + FullTimestamp: true, + }) + + log.SetOutput(os.Stdout) +} + +func InfoWithFields(message string, fields Fields) { + log.WithFields(fields).Info(message) +} + +func DebugWithFields(message string, fields Fields) { + log.WithFields(fields).Debug(message) +} + +func ErrorWithFields(message string, fields Fields) { + log.WithFields(fields).Error(message) +} + +func FatalWithFields(message string, fields Fields) { + log.WithFields(fields).Fatal(message) +} + +func PanicWithFields(message string, fields Fields) { + log.WithFields(fields).Panic(message) +} + +func Info(args ...interface{}) { + log.Info(args...) +} + +func Debug(args ...interface{}) { + log.Debug(args...) +} + +func Error(args ...interface{}) { + log.Error(args...) +} + +func Fatal(args ...interface{}) { + log.Fatal(args...) +} + +func Panic(args ...interface{}) { + log.Panic(args...) +} diff --git a/pkg/queue/redis.go b/pkg/queue/redis.go new file mode 100644 index 0000000..4b90d92 --- /dev/null +++ b/pkg/queue/redis.go @@ -0,0 +1,44 @@ +package queue + +import ( + "zapmeow/pkg/logger" + + "github.com/go-redis/redis" +) + +type Queue interface { + Enqueue(queueName string, values ...interface{}) error + Dequeue(queueName string) ([]byte, error) +} + +type queue struct { + client *redis.Client +} + +func NewQueue(addr string, password string) *queue { + client := redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: 0, + }) + if _, err := client.Ping().Result(); err != nil { + logger.Fatal(err) + } + return &queue{ + client: client, + } +} + +func (q *queue) Enqueue(queueName string, values ...interface{}) error { + return q.client.LPush(queueName, values).Err() +} + +func (q *queue) Dequeue(queueName string) ([]byte, error) { + result, err := q.client.LPop(queueName).Bytes() + if err != nil && err == redis.Nil { + return nil, nil + } else if err != nil { + return nil, err + } + return result, nil +} diff --git a/pkg/whatsapp/whatsapp.go b/pkg/whatsapp/whatsapp.go new file mode 100644 index 0000000..eafee1f --- /dev/null +++ b/pkg/whatsapp/whatsapp.go @@ -0,0 +1,488 @@ +package whatsapp + +import ( + "context" + "errors" + "fmt" + "time" + "zapmeow/pkg/logger" + + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow" + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/store" + "go.mau.fi/whatsmeow/store/sqlstore" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" + waLog "go.mau.fi/whatsmeow/util/log" + "google.golang.org/protobuf/proto" +) + +type Client = whatsmeow.Client + +type JID = types.JID + +type Instance struct { + ID string + Client *Client + QrCodeRateLimit uint16 +} + +type Message struct { + InstanceID string + Body string + SenderJID string + ChatJID string + MessageID string + FromMe bool + Timestamp time.Time + MediaType *MediaType + Media *[]byte + Mimetype *string +} + +type MediaType int + +const ( + Audio MediaType = iota + Image + Document + Sticker + Video +) + +func (m MediaType) String() string { + switch m { + case Audio: + return "audio" + case Document: + return "document" + case Sticker: + return "sticker" + case Video: + return "video" + case Image: + return "image" + } + return "unknown" +} + +type ContactInfo struct { + Phone string `json:"phone"` + Name string `json:"name"` + Status string `json:"status"` + Picture string `json:"picture"` +} + +type MessageResponse struct { + ID string + Sender JID + Timestamp time.Time +} + +type DownloadResponse struct { + Data []byte + Type MediaType + Mimetype string +} + +type UploadResponse struct { + URL string + DirectPath string + Mimetype MediaType + MediaKey []byte + FileEncSHA256 []byte + FileSHA256 []byte + FileLength uint64 +} + +type IsOnWhatsAppResponse struct { + Query string `json:"query"` + Phone string `json:"phone"` + IsRegistered bool `json:"is_registered"` +} + +type WhatsApp interface { + CreateInstance(id string) *Instance + CreateInstanceFromDevice(id string, jid JID) *Instance + IsLoggedIn(instance *Instance) bool + IsConnected(instance *Instance) bool + Disconnect(instance *Instance) + Logout(instance *Instance) error + EventHandler(instance *Instance, handler func(evt interface{})) + InitInstance(instance *Instance, qrcodeHandler func(evt string, qrcode string, err error)) error + SendTextMessage(instance *Instance, jid JID, text string) (MessageResponse, error) + SendAudioMessage(instance *Instance, jid JID, audioURL *dataurl.DataURL, mimitype string) (MessageResponse, error) + SendImageMessage(instance *Instance, jid JID, imageURL *dataurl.DataURL, mimitype string) (MessageResponse, error) + GetContactInfo(instance *Instance, jid JID) (*ContactInfo, error) + ParseEventMessage(instance *Instance, message *events.Message) (Message, error) + IsOnWhatsApp(instance *Instance, phones []string) ([]IsOnWhatsAppResponse, error) +} + +type whatsApp struct { + container *sqlstore.Container +} + +func NewWhatsApp(databasePath string) *whatsApp { + dbLog := waLog.Stdout("Database", "DEBUG", true) + container, err := sqlstore.New("sqlite3", "file:"+databasePath+"?_foreign_keys=on", dbLog) + if err != nil { + logger.Fatal(err) + } + return &whatsApp{container: container} +} + +func (w *whatsApp) CreateInstance(id string) *Instance { + client := w.createClient(w.container.NewDevice()) + return &Instance{ + ID: id, + Client: client, + QrCodeRateLimit: 10, + } +} + +func (w *whatsApp) CreateInstanceFromDevice(id string, jid JID) *Instance { + device, _ := w.container.GetDevice(JID{ + User: jid.User, + Agent: jid.Agent, + Device: jid.Device, + Server: jid.Server, + AD: jid.AD, + }) + if device != nil { + client := w.createClient(device) + return &Instance{ + ID: id, + Client: client, + QrCodeRateLimit: 10, + } + } + return w.CreateInstance(id) +} + +func (w *whatsApp) IsLoggedIn(instance *Instance) bool { + return instance.Client.IsLoggedIn() +} + +func (w *whatsApp) IsConnected(instance *Instance) bool { + return instance.Client.IsConnected() +} + +func (w *whatsApp) Disconnect(instance *Instance) { + instance.Client.Disconnect() +} + +func (w *whatsApp) Connect(instance *Instance) { + instance.Client.Disconnect() +} + +func (w *whatsApp) Logout(instance *Instance) error { + return instance.Client.Logout() +} + +func (w *whatsApp) EventHandler(instance *Instance, handler func(evt interface{})) { + instance.Client.AddEventHandler(handler) +} + +func (w *whatsApp) InitInstance(instance *Instance, qrcodeHandler func(evt string, qrcode string, err error)) error { + if instance.Client.Store.ID == nil { + go w.generateQrcode(instance, qrcodeHandler) + } else { + err := instance.Client.Connect() + if err != nil { + return err + } + + if !instance.Client.WaitForConnection(5 * time.Second) { + return errors.New("websocket didn't reconnect within 5 seconds of failed") + } + } + + return nil +} + +func (w *whatsApp) SendTextMessage(instance *Instance, jid JID, text string) (MessageResponse, error) { + message := &waProto.Message{ + ExtendedTextMessage: &waProto.ExtendedTextMessage{ + Text: &text, + }, + } + return w.sendMessage(instance, jid, message) +} + +func (w *whatsApp) SendAudioMessage(instance *Instance, jid JID, audioURL *dataurl.DataURL, mimitype string) (MessageResponse, error) { + uploaded, err := w.uploadMedia(instance, audioURL, Audio) + if err != nil { + return MessageResponse{}, err + } + message := &waProto.Message{ + AudioMessage: &waProto.AudioMessage{ + Ptt: proto.Bool(true), + Url: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimitype), + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(audioURL.Data))), + }, + } + return w.sendMessage(instance, jid, message) +} + +func (w *whatsApp) SendImageMessage(instance *Instance, jid JID, imageURL *dataurl.DataURL, mimitype string) (MessageResponse, error) { + uploaded, err := w.uploadMedia(instance, imageURL, Image) + if err != nil { + return MessageResponse{}, err + } + message := &waProto.Message{ + ImageMessage: &waProto.ImageMessage{ + Url: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimitype), + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(imageURL.Data))), + }, + } + return w.sendMessage(instance, jid, message) +} + +func (w *whatsApp) IsOnWhatsApp(instance *Instance, phones []string) ([]IsOnWhatsAppResponse, error) { + isOnWhatsAppResponse, err := instance.Client.IsOnWhatsApp(phones) + if err != nil { + return nil, err + } + + data := make([]IsOnWhatsAppResponse, len(isOnWhatsAppResponse)) + for _, resp := range isOnWhatsAppResponse { + data = append(data, IsOnWhatsAppResponse{ + Query: resp.Query, + IsRegistered: resp.IsIn, + Phone: resp.JID.User, + }) + } + + return data, nil +} + +func (w *whatsApp) sendMessage(instance *Instance, jid JID, message *waProto.Message) (MessageResponse, error) { + resp, err := instance.Client.SendMessage(context.Background(), jid, message) + if err != nil { + return MessageResponse{}, err + } + + return MessageResponse{ + ID: resp.ID, + Sender: *instance.Client.Store.ID, + Timestamp: resp.Timestamp, + }, nil +} + +func (w *whatsApp) GetContactInfo(instance *Instance, jid JID) (*ContactInfo, error) { + userInfo, err := instance.Client.GetUserInfo([]JID{jid}) + if err != nil { + return nil, err + } + + contactInfo, err := instance.Client.Store.Contacts.GetContact(jid) + if err != nil { + return nil, err + } + + profilePictureInfo, err := instance.Client.GetProfilePictureInfo( + jid, + &whatsmeow.GetProfilePictureParams{}, + ) + + profilePictureURL := "" + if profilePictureInfo != nil { + profilePictureURL = profilePictureInfo.URL + } + + return &ContactInfo{ + Phone: jid.User, + Name: contactInfo.PushName, + Status: userInfo[jid].Status, + Picture: profilePictureURL, + }, nil +} + +func (w *whatsApp) ParseEventMessage(instance *Instance, message *events.Message) (Message, error) { + media, err := w.downloadMedia( + instance, + message.Message, + ) + + if err != nil && media == nil { + return Message{}, err + } + + text := w.getTextMessage(message.Message) + base := Message{ + InstanceID: instance.ID, + Body: text, + MessageID: message.Info.ID, + ChatJID: message.Info.Chat.User, + SenderJID: message.Info.Sender.User, + FromMe: message.Info.MessageSource.IsFromMe, + Timestamp: message.Info.Timestamp, + } + + if media != nil && err == nil { + base.MediaType = &media.Type + base.Mimetype = &media.Mimetype + base.Media = &media.Data + return base, nil + } + + return base, nil +} + +func (w *whatsApp) createClient(deviceStore *store.Device) *whatsmeow.Client { + // TODO: verificar se o ambiente é de produção ou dev + log := waLog.Stdout("Client", "DEBUG", true) + client := whatsmeow.NewClient(deviceStore, log) + return client +} + +func (w *whatsApp) uploadMedia(instance *Instance, media *dataurl.DataURL, mediaType MediaType) (*UploadResponse, error) { + var mType whatsmeow.MediaType + switch mediaType { + case Image: + mType = whatsmeow.MediaImage + case Audio: + mType = whatsmeow.MediaAudio + default: + return nil, errors.New("unknown media type") + } + + uploaded, err := instance.Client.Upload(context.Background(), media.Data, mType) + if err != nil { + return nil, err + } + + return &UploadResponse{ + URL: uploaded.URL, + Mimetype: mediaType, + DirectPath: uploaded.DirectPath, + MediaKey: uploaded.MediaKey, + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: uploaded.FileLength, + }, nil +} + +func (w *whatsApp) downloadMedia(instance *Instance, message *waProto.Message) (*DownloadResponse, error) { + document := message.GetDocumentMessage() + if document != nil { + data, err := instance.Client.Download(document) + if err != nil { + return &DownloadResponse{Type: Document}, err + } + + return &DownloadResponse{ + Data: data, + Type: Document, + Mimetype: document.GetMimetype(), + }, nil + } + + audio := message.GetAudioMessage() + if audio != nil { + data, err := instance.Client.Download(audio) + if err != nil { + return &DownloadResponse{Type: Audio}, err + } + + return &DownloadResponse{ + Data: data, + Type: Audio, + Mimetype: audio.GetMimetype(), + }, nil + } + + image := message.GetImageMessage() + if image != nil { + data, err := instance.Client.Download(image) + if err != nil { + return &DownloadResponse{Type: Image}, err + } + + return &DownloadResponse{ + Data: data, + Type: Image, + Mimetype: image.GetMimetype(), + }, nil + } + + sticker := message.GetStickerMessage() + if sticker != nil { + data, err := instance.Client.Download(sticker) + if err != nil { + return &DownloadResponse{Type: Sticker}, err + } + + return &DownloadResponse{ + Data: data, + Type: Sticker, + Mimetype: sticker.GetMimetype(), + }, nil + } + + video := message.GetVideoMessage() + if video != nil { + data, err := instance.Client.Download(video) + if err != nil { + return &DownloadResponse{Type: Video}, err + } + + return &DownloadResponse{ + Data: data, + Type: Video, + Mimetype: video.GetMimetype(), + }, nil + } + + return nil, nil +} + +func (w *whatsApp) getTextMessage(message *waProto.Message) string { + extendedTextMessage := message.GetExtendedTextMessage() + if extendedTextMessage != nil { + return *extendedTextMessage.Text + } + return message.GetConversation() +} + +func (w *whatsApp) generateQrcode(instance *Instance, qrcodeHandler func(evt string, qrcode string, err error)) { + qrChan, err := instance.Client.GetQRChannel(context.Background()) + if err != nil { + if !errors.Is(err, whatsmeow.ErrQRStoreContainsID) { + errMessage := fmt.Sprintf("Failed to get qr channel. %s", err) + qrcodeHandler("error", "", errors.New(errMessage)) + } + } else { + err = instance.Client.Connect() + if err != nil { + errMessage := fmt.Sprintf("Failed to connect client to WhatsApp websocket. %s", err) + qrcodeHandler("error", "", errors.New(errMessage)) + } else { + for evt := range qrChan { + if instance.QrCodeRateLimit == 0 { + qrcodeHandler("rate-limit", "", nil) + return + } + + switch evt.Event { + case "code": + instance.QrCodeRateLimit -= 1 + qrcodeHandler("code", evt.Code, nil) + default: + qrcodeHandler(evt.Event, "", evt.Error) + } + } + } + } +} diff --git a/pkg/zapmeow/zapmeow.go b/pkg/zapmeow/zapmeow.go new file mode 100644 index 0000000..7fc8955 --- /dev/null +++ b/pkg/zapmeow/zapmeow.go @@ -0,0 +1,55 @@ +package zapmeow + +import ( + "sync" + "zapmeow/config" + "zapmeow/pkg/database" + "zapmeow/pkg/queue" + "zapmeow/pkg/whatsapp" +) + +type ZapMeow struct { + Database database.Database + Queue queue.Queue + Config config.Config + Instances *sync.Map + Wg *sync.WaitGroup + Mutex *sync.Mutex + StopCh *chan struct{} +} + +func NewZapMeow( + database database.Database, + queue queue.Queue, + config config.Config, + instances *sync.Map, + wg *sync.WaitGroup, + mutex *sync.Mutex, + stopCh *chan struct{}, +) *ZapMeow { + return &ZapMeow{ + Database: database, + Queue: queue, + Instances: instances, + Config: config, + Wg: wg, + Mutex: mutex, + StopCh: stopCh, + } +} + +func (a *ZapMeow) LoadInstance(instanceID string) *whatsapp.Instance { + value, _ := a.Instances.Load(instanceID) + if value != nil { + return value.(*whatsapp.Instance) + } + return nil +} + +func (a *ZapMeow) StoreInstance(instanceID string, instance *whatsapp.Instance) { + a.Instances.Store(instanceID, instance) +} + +func (a *ZapMeow) DeleteInstance(instanceID string) { + a.Instances.Delete(instanceID) +} diff --git a/repositories/account_repository.go b/repositories/account_repository.go deleted file mode 100644 index d969bd2..0000000 --- a/repositories/account_repository.go +++ /dev/null @@ -1,57 +0,0 @@ -package repositories - -import ( - "zapmeow/models" - - "gorm.io/gorm" -) - -type AccountRepository interface { - CreateAccount(account *models.Account) error - GetConnectedAccounts() ([]models.Account, error) - GetAccountByInstanceID(instanceID string) (*models.Account, error) - UpdateAccount(instanceID string, data map[string]interface{}) error -} - -type accountRepository struct { - db *gorm.DB -} - -func NewAccountRepository(db *gorm.DB) *accountRepository { - return &accountRepository{db: db} -} - -func (repo *accountRepository) CreateAccount(account *models.Account) error { - return repo.db.Create(account).Error -} - -func (repo *accountRepository) GetConnectedAccounts() ([]models.Account, error) { - var accounts []models.Account - repo.db.Where("status = ?", "CONNECTED").Find(&accounts) - return accounts, nil -} - -func (repo *accountRepository) GetAccountByInstanceID(instanceID string) (*models.Account, error) { - var account models.Account - result := repo.db.Where("instance_id = ?", instanceID).First(&account) - if result.Error != nil { - if result.Error != gorm.ErrRecordNotFound { - return nil, result.Error - } - return nil, nil - } - return &account, nil -} - -func (repo *accountRepository) UpdateAccount(instanceID string, data map[string]interface{}) error { - var account models.Account - if result := repo.db.Where("instance_id = ?", instanceID).First(&account); result.Error != nil { - return result.Error - } - - if err := repo.db.Model(&account).Updates(data).Error; err != nil { - return err - } - - return nil -} diff --git a/repositories/message_repository.go b/repositories/message_repository.go deleted file mode 100644 index c18dd20..0000000 --- a/repositories/message_repository.go +++ /dev/null @@ -1,54 +0,0 @@ -package repositories - -import ( - "zapmeow/models" - - "gorm.io/gorm" -) - -type MessageRepository interface { - CreateMessage(message *models.Message) error - CreateMessages(messages *[]models.Message) error - GetChatMessages(instanceID string, chatJID string) (*[]models.Message, error) - CountChatMessages(instanceID string, chatJID string) (int64, error) - DeleteMessagesByInstanceID(instanceID string) error -} - -type messageRepository struct { - db *gorm.DB -} - -func NewMessageRepository(db *gorm.DB) *messageRepository { - return &messageRepository{db: db} -} - -func (repo *messageRepository) CreateMessage(message *models.Message) error { - return repo.db.Create(message).Error -} - -func (repo *messageRepository) CreateMessages(messages *[]models.Message) error { - return repo.db.Create(messages).Error -} - -func (repo *messageRepository) CountChatMessages(instanceID string, chatJID string) (int64, error) { - var count int64 - if result := repo.db.Model(&models.Message{}).Where("instance_id = ? AND chat_jid = ?", instanceID, chatJID).Count(&count); result.Error != nil { - return 0, result.Error - } - return count, nil -} - -func (repo *messageRepository) GetChatMessages(instanceID string, chatJID string) (*[]models.Message, error) { - var messages []models.Message - if result := repo.db.Where("instance_id = ? AND chat_jid = ?", instanceID, chatJID).Order("timestamp DESC").Find(&messages); result.Error != nil { - return nil, result.Error - } - return &messages, nil -} - -func (repo *messageRepository) DeleteMessagesByInstanceID(instanceID string) error { - if result := repo.db.Where("instance_id = ?", instanceID).Unscoped().Delete(&models.Message{}); result.Error != nil { - return result.Error - } - return nil -} diff --git a/routes/routes.go b/routes/routes.go deleted file mode 100644 index 6a4e5b1..0000000 --- a/routes/routes.go +++ /dev/null @@ -1,87 +0,0 @@ -package routes - -import ( - "zapmeow/configs" - "zapmeow/controllers" - "zapmeow/services" - - docs "zapmeow/docs" - - "github.com/gin-gonic/gin" - swaggerfiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" -) - -func SetupRouter( - app *configs.ZapMeow, - wppService services.WppService, - messageService services.MessageService, - accountService services.AccountService, -) *gin.Engine { - docs.SwaggerInfo.BasePath = "/api" - - var router *gin.Engine - if app.Config.Environment == configs.Development { - router = gin.New() - } else { - router = gin.Default() - } - - getQrCodeController := controllers.NewGetQrCodeController( - app, - wppService, - messageService, - accountService, - ) - logoutController := controllers.NewLogoutController( - app, - wppService, - accountService, - ) - getStatusController := controllers.NewGetStatusController( - app, - wppService, - accountService, - ) - getProfileInfoController := controllers.NewGetProfileInfoController( - wppService, - ) - getContactInfoController := controllers.NewGetContactInfoController( - wppService, - ) - checkPhonesController := controllers.NewCheckPhonesController( - wppService, - ) - getMessagesController := controllers.NewGetMessagesController( - wppService, - messageService, - ) - sendTextMessageController := controllers.NewSendTextMessageController( - wppService, - messageService, - ) - sendImageMessageController := controllers.NewSendImageMessageController( - wppService, - messageService, - ) - sendAudioMessageController := controllers.NewSendAudioMessageController( - wppService, - messageService, - ) - - group := router.Group("/api") - - group.GET("/:instanceId/qrcode", getQrCodeController.Handler) - group.POST("/:instanceId/logout", logoutController.Handler) - group.GET("/:instanceId/status", getStatusController.Handler) - group.GET("/:instanceId/contact/info", getContactInfoController.Handler) - group.GET("/:instanceId/profile", getProfileInfoController.Handler) - group.POST("/:instanceId/check/phones", checkPhonesController.Handler) - group.POST("/:instanceId/chat/messages", getMessagesController.Handler) - group.POST("/:instanceId/chat/send/text", sendTextMessageController.Handler) - group.POST("/:instanceId/chat/send/image", sendImageMessageController.Handler) - group.POST("/:instanceId/chat/send/audio", sendAudioMessageController.Handler) - group.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) - - return router -} diff --git a/services/message_service.go b/services/message_service.go deleted file mode 100644 index e7da0db..0000000 --- a/services/message_service.go +++ /dev/null @@ -1,100 +0,0 @@ -package services - -import ( - "encoding/base64" - "io/ioutil" - "mime" - "path/filepath" - "time" - "zapmeow/configs" - "zapmeow/models" - "zapmeow/repositories" -) - -type MessageService interface { - CreateMessage(message *models.Message) error - CreateMessages(messages *[]models.Message) error - GetChatMessages(instanceID string, chatJID string) (*[]models.Message, error) - CountChatMessages(instanceID string, chatJID string) (int64, error) - DeleteMessagesByInstanceID(instanceID string) error - ToJSON(message models.Message) Message -} - -type Message struct { - ID uint - Sender string - Chat string - MessageID string - FromMe bool - Timestamp time.Time - Body string - MediaType string - MediaData *struct { - Mimetype string - Base64 string - } -} - -type messageService struct { - messageRep repositories.MessageRepository - log configs.Logger -} - -func NewMessageService(messageRep repositories.MessageRepository, log configs.Logger) *messageService { - return &messageService{ - messageRep: messageRep, - log: log, - } -} - -func (m *messageService) CreateMessage(message *models.Message) error { - return m.messageRep.CreateMessage(message) -} - -func (m *messageService) CreateMessages(messages *[]models.Message) error { - return m.messageRep.CreateMessages(messages) -} - -func (m *messageService) GetChatMessages(instanceID string, chatJID string) (*[]models.Message, error) { - return m.messageRep.GetChatMessages(instanceID, chatJID) -} - -func (m *messageService) CountChatMessages(instanceID string, chatJID string) (int64, error) { - return m.messageRep.CountChatMessages(instanceID, chatJID) -} - -func (m *messageService) DeleteMessagesByInstanceID(instanceID string) error { - return m.messageRep.DeleteMessagesByInstanceID(instanceID) -} - -func (m *messageService) ToJSON(message models.Message) Message { - messageJson := Message{ - ID: message.ID, - Sender: message.SenderJID, - Chat: message.ChatJID, - MessageID: message.MessageID, - FromMe: message.FromMe, - Timestamp: message.Timestamp, - Body: message.Body, - MediaType: message.MediaType, - } - - if message.MediaType != "" { - data, err := ioutil.ReadFile(message.MediaPath) - if err != nil { - m.log.Error("Error reading the file. ", err) - } else { - mimetype := mime.TypeByExtension(filepath.Ext(message.MediaPath)) - base64 := base64.StdEncoding.EncodeToString(data) - messageJson.MediaData = &struct { - Mimetype string - Base64 string - }{ - Mimetype: mimetype, - Base64: base64, - } - } - } - - return messageJson -} diff --git a/services/wpp_service.go b/services/wpp_service.go deleted file mode 100644 index b5c0089..0000000 --- a/services/wpp_service.go +++ /dev/null @@ -1,681 +0,0 @@ -package services - -import ( - "context" - "errors" - "os" - "time" - "zapmeow/configs" - "zapmeow/models" - "zapmeow/queues" - "zapmeow/utils" - - "github.com/vincent-petithory/dataurl" - "go.mau.fi/whatsmeow" - waProto "go.mau.fi/whatsmeow/binary/proto" - "go.mau.fi/whatsmeow/store" - "go.mau.fi/whatsmeow/types" - "go.mau.fi/whatsmeow/types/events" - waLog "go.mau.fi/whatsmeow/util/log" - "google.golang.org/protobuf/proto" -) - -type wppService struct { - app *configs.ZapMeow - messageService MessageService - accountService AccountService - log configs.Logger -} - -type ContactInfo struct { - Phone string - Name string - Status string - Picture string -} - -type SendMessageResponse struct { - ID string - Sender types.JID - Timestamp time.Time -} - -type DownloadedMedia struct { - Data []byte - Type MediaType - Mimetype string -} - -type ParsedEventMessage struct { - InstanceID string - Body string - SenderJID string - ChatJID string - MessageID string - Timestamp time.Time - FromMe bool - MediaType *MediaType - Media *[]byte - Mimetype *string -} - -type MediaType int - -const ( - Audio MediaType = iota - Image - Document - Sticker - Video -) - -func (m MediaType) String() string { - switch m { - case Audio: - return "audio" - case Document: - return "document" - case Sticker: - return "sticker" - case Video: - return "video" - case Image: - return "image" - } - return "unknown" -} - -type WppService interface { - GetInstance(instanceID string) (*configs.Instance, error) - GetAuthenticatedInstance(instanceID string) (*configs.Instance, error) - GetContactInfo(instanceID string, jid types.JID) (*ContactInfo, error) - SendMessage(instanceID string, jid types.JID, message *waProto.Message) (SendMessageResponse, error) - SendTextMessage(instanceID string, jid types.JID, message string) (SendMessageResponse, error) - SendAudioMessage(instanceID string, jid types.JID, audio *dataurl.DataURL, mimitype string) (SendMessageResponse, error) - SendImageMessage(instanceID string, jid types.JID, image *dataurl.DataURL, mimitype string) (SendMessageResponse, error) - ParseEventMessage(instance *configs.Instance, message *events.Message) (ParsedEventMessage, error) - Logout(instanceID string) error - destroyInstance(instanceID string) error - getTextMessage(message *waProto.Message) string - downloadMedia(instance *configs.Instance, message *waProto.Message) (*DownloadedMedia, error) - uploadMedia(instanceID string, media *dataurl.DataURL, mediaType MediaType) (*whatsmeow.UploadResponse, error) -} - -func NewWppService( - app *configs.ZapMeow, - messageService MessageService, - accountService AccountService, - log configs.Logger, -) *wppService { - return &wppService{ - app: app, - messageService: messageService, - accountService: accountService, - log: log, - } -} - -func (w *wppService) GetInstance(instanceID string) (*configs.Instance, error) { - instance := w.app.LoadInstance(instanceID) - - if instance != nil { - return instance, nil - } - - client, err := w.getClient(instanceID) - if err != nil { - return nil, err - } - - w.app.StoreInstance(instanceID, &configs.Instance{ - ID: instanceID, - Client: client, - QrCodeRateLimit: 10, - }) - - instance = w.app.LoadInstance(instanceID) - - instance.Client.AddEventHandler(func(evt interface{}) { - w.eventHandler(instanceID, evt) - }) - - if instance.Client.Store.ID == nil { - go w.qrcode(instanceID) - } else { - err := instance.Client.Connect() - if err != nil { - return nil, err - } - - if !instance.Client.WaitForConnection(5 * time.Second) { - return nil, errors.New("websocket didn't reconnect within 5 seconds of failed") - } - } - - return instance, nil -} - -func (w *wppService) GetAuthenticatedInstance(instanceID string) (*configs.Instance, error) { - instance, err := w.GetInstance(instanceID) - if err != nil { - return nil, err - } - - if !instance.Client.IsConnected() { - return nil, errors.New("unconnected instance") - } - - if !instance.Client.IsLoggedIn() { - return nil, errors.New("unauthenticated instance") - } - - return instance, nil -} - -func (w *wppService) SendTextMessage(instanceID string, jid types.JID, text string) (SendMessageResponse, error) { - message := &waProto.Message{ - ExtendedTextMessage: &waProto.ExtendedTextMessage{ - Text: &text, - }, - } - return w.SendMessage(instanceID, jid, message) -} - -func (w *wppService) SendAudioMessage(instanceID string, jid types.JID, audioURL *dataurl.DataURL, mimitype string) (SendMessageResponse, error) { - uploaded, err := w.uploadMedia(instanceID, audioURL, Audio) - if err != nil { - return SendMessageResponse{}, err - } - message := &waProto.Message{ - AudioMessage: &waProto.AudioMessage{ - Ptt: proto.Bool(true), - Url: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - Mimetype: proto.String(mimitype), - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(audioURL.Data))), - }, - } - return w.SendMessage(instanceID, jid, message) -} - -func (w *wppService) SendImageMessage(instanceID string, jid types.JID, imageURL *dataurl.DataURL, mimitype string) (SendMessageResponse, error) { - uploaded, err := w.uploadMedia(instanceID, imageURL, Image) - if err != nil { - return SendMessageResponse{}, err - } - message := &waProto.Message{ - ImageMessage: &waProto.ImageMessage{ - Url: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - Mimetype: proto.String(mimitype), - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(imageURL.Data))), - }, - } - return w.SendMessage(instanceID, jid, message) -} - -func (w *wppService) SendMessage(instanceID string, jid types.JID, message *waProto.Message) (SendMessageResponse, error) { - instance, err := w.GetAuthenticatedInstance(instanceID) - if err != nil { - return SendMessageResponse{}, err - } - - resp, err := instance.Client.SendMessage(context.Background(), jid, message) - if err != nil { - return SendMessageResponse{}, err - } - - return SendMessageResponse{ - ID: resp.ID, - Sender: *instance.Client.Store.ID, - Timestamp: resp.Timestamp, - }, nil -} - -func (w *wppService) ParseEventMessage(instance *configs.Instance, message *events.Message) (ParsedEventMessage, error) { - media, err := w.downloadMedia( - instance, - message.Message, - ) - - if err != nil && media == nil { - return ParsedEventMessage{}, err - } - - text := w.getTextMessage(message.Message) - base := ParsedEventMessage{ - InstanceID: instance.ID, - Body: text, - MessageID: message.Info.ID, - ChatJID: message.Info.Chat.User, - SenderJID: message.Info.Sender.User, - FromMe: message.Info.MessageSource.IsFromMe, - Timestamp: message.Info.Timestamp, - } - - if media != nil && err == nil { - base.MediaType = &media.Type - base.Mimetype = &media.Mimetype - base.Media = &media.Data - return base, nil - } - - return base, nil -} - -func (w *wppService) uploadMedia(instanceID string, media *dataurl.DataURL, mediaType MediaType) (*whatsmeow.UploadResponse, error) { - instance, err := w.GetAuthenticatedInstance(instanceID) - if err != nil { - return nil, err - } - - var mType whatsmeow.MediaType - switch mediaType { - case Image: - mType = whatsmeow.MediaImage - case Audio: - mType = whatsmeow.MediaAudio - default: - return nil, errors.New("unknown media type") - } - - uploaded, err := instance.Client.Upload(context.Background(), media.Data, mType) - if err != nil { - return nil, err - } - - return &uploaded, nil -} - -func (m *wppService) downloadMedia(instance *configs.Instance, message *waProto.Message) (*DownloadedMedia, error) { - dirPath := utils.MakeAccountStoragePath(instance.ID) - err := os.MkdirAll(dirPath, 0751) - if err != nil { - return nil, err - } - - document := message.GetDocumentMessage() - if document != nil { - data, err := instance.Client.Download(document) - if err != nil { - return &DownloadedMedia{Type: Document}, err - } - - return &DownloadedMedia{ - Data: data, - Type: Document, - Mimetype: document.GetMimetype(), - }, nil - } - - audio := message.GetAudioMessage() - if audio != nil { - data, err := instance.Client.Download(audio) - if err != nil { - return &DownloadedMedia{Type: Audio}, err - } - - return &DownloadedMedia{ - Data: data, - Type: Audio, - Mimetype: audio.GetMimetype(), - }, nil - } - - image := message.GetImageMessage() - if image != nil { - data, err := instance.Client.Download(image) - if err != nil { - return &DownloadedMedia{Type: Image}, err - } - - return &DownloadedMedia{ - Data: data, - Type: Image, - Mimetype: image.GetMimetype(), - }, nil - } - - sticker := message.GetStickerMessage() - if sticker != nil { - data, err := instance.Client.Download(sticker) - if err != nil { - return &DownloadedMedia{Type: Sticker}, err - } - - return &DownloadedMedia{ - Data: data, - Type: Sticker, - Mimetype: sticker.GetMimetype(), - }, nil - } - - video := message.GetVideoMessage() - if video != nil { - data, err := instance.Client.Download(video) - if err != nil { - return &DownloadedMedia{Type: Video}, err - } - - return &DownloadedMedia{ - Data: data, - Type: Video, - Mimetype: video.GetMimetype(), - }, nil - } - - return nil, nil -} - -func (m *wppService) getTextMessage(message *waProto.Message) string { - extendedTextMessage := message.GetExtendedTextMessage() - if extendedTextMessage != nil { - return *extendedTextMessage.Text - } - return message.GetConversation() -} - -func (w *wppService) destroyInstance(instanceID string) error { - instance, err := w.GetInstance(instanceID) - if err != nil { - return err - } - - err = w.accountService.DeleteAccountMessages(instanceID) - if err != nil { - return err - } - - instance.Client.Disconnect() - w.app.DeleteInstance(instanceID) - - return nil -} - -func (w *wppService) Logout(instanceID string) error { - instance, err := w.GetAuthenticatedInstance(instanceID) - if err != nil { - return err - } - - err = instance.Client.Logout() - if err != nil { - return err - } - - err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ - "Status": "UNPAIRED", - }) - if err != nil { - return err - } - - return w.destroyInstance(instanceID) -} - -func (w *wppService) GetContactInfo(instanceID string, jid types.JID) (*ContactInfo, error) { - instance, err := w.GetAuthenticatedInstance(instanceID) - if err != nil { - return nil, err - } - - userInfo, err := instance.Client.GetUserInfo([]types.JID{jid}) - if err != nil { - return nil, err - } - - ctInfo, err := instance.Client.Store.Contacts.GetContact(jid) - if err != nil { - return nil, err - } - - profilePictureInfo, err := instance.Client.GetProfilePictureInfo( - jid, - &whatsmeow.GetProfilePictureParams{}, - ) - - profilePictureURL := "" - if profilePictureInfo != nil { - profilePictureURL = profilePictureInfo.URL - } - - return &ContactInfo{ - Phone: jid.User, - Name: ctInfo.PushName, - Status: userInfo[jid].Status, - Picture: profilePictureURL, - }, nil -} - -func (w *wppService) getClient(instanceID string) (*whatsmeow.Client, error) { - account, err := w.accountService.GetAccountByInstanceID(instanceID) - if err != nil { - return nil, err - } - - if account == nil { - err := w.accountService.CreateAccount(&models.Account{ - InstanceID: instanceID, - }) - - if err != nil { - return nil, err - } - return createClient( - w.app.Config, - w.app.WhatsmeowContainer.NewDevice(), - ), nil - } else if account.Status != "CONNECTED" { - return createClient( - w.app.Config, - w.app.WhatsmeowContainer.NewDevice(), - ), nil - } - - device, err := w.app.WhatsmeowContainer.GetDevice(types.JID{ - User: account.User, - Agent: account.Agent, - Device: account.Device, - Server: account.Server, - AD: account.AD, - }) - if err != nil { - return nil, err - } - - if device != nil { - return createClient( - w.app.Config, - device, - ), nil - } - - device = w.app.WhatsmeowContainer.NewDevice() - return createClient( - w.app.Config, - device, - ), nil -} - -func (w *wppService) qrcode(instanceID string) { - instance := w.app.LoadInstance(instanceID) - if instance.Client.Store.ID == nil { - qrChan, err := instance.Client.GetQRChannel(context.Background()) - if err != nil { - if !errors.Is(err, whatsmeow.ErrQRStoreContainsID) { - w.log.Error("Failed to get qr channel. ", err) - } - } else { - err = instance.Client.Connect() - if err != nil { - w.log.Error("Failed to connect client to WhatsApp websocket. ", err) - return - } - - for evt := range qrChan { - if instance.QrCodeRateLimit == 0 { - err := w.destroyInstance(instanceID) - if err != nil { - w.log.Error("Failed to destroy instance. ", err) - } - return - } - - switch evt.Event { - case "success": - return - case "timeout": - { - err := w.accountService.UpdateAccount(instanceID, map[string]interface{}{ - "QrCode": "", - "Status": "TIMEOUT", - }) - if err != nil { - w.log.Error("Failed to update account. ", err) - } - - w.app.DeleteInstance(instanceID) - } - case "code": - { - instance.QrCodeRateLimit -= 1 - err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ - "QrCode": evt.Code, - "Status": "UNPAIRED", - "WasSynced": false, - }) - if err != nil { - w.log.Error("Failed to update account. ", err) - } - } - } - } - } - } -} - -func (w *wppService) eventHandler(instanceID string, rawEvt interface{}) { - switch evt := rawEvt.(type) { - case *events.Message: - w.handleMessage(instanceID, evt) - case *events.HistorySync: - w.handleHistorySync(instanceID, evt) - case *events.Connected: - w.handleConnected(instanceID) - case *events.LoggedOut: - w.handleLoggedOut(instanceID) - } -} - -func (w *wppService) handleHistorySync(instanceID string, evt *events.HistorySync) { - history, _ := proto.Marshal(evt.Data) - - queue := queues.NewHistorySyncQueue(w.app, w.log) - err := queue.Enqueue(queues.HistorySyncQueueData{ - History: history, - InstanceID: instanceID, - }) - - if err != nil { - w.log.Error("Failed to add history sync to queue. ", err) - } -} - -func (w *wppService) handleConnected(instanceID string) { - var instance = w.app.LoadInstance(instanceID) - err := w.accountService.UpdateAccount(instanceID, map[string]interface{}{ - "User": instance.Client.Store.ID.User, - "Agent": instance.Client.Store.ID.Agent, - "Device": instance.Client.Store.ID.Device, - "Server": instance.Client.Store.ID.Server, - "AD": instance.Client.Store.ID.AD, - "InstanceID": instance.ID, - "Status": "CONNECTED", - "QrCode": "", - "WasSynced": false, - }) - - if err != nil { - w.log.Error("Failed to update account. ", err) - } -} - -func (w *wppService) handleLoggedOut(instanceID string) { - err := w.destroyInstance(instanceID) - if err != nil { - w.log.Error(err) - } - - err = w.accountService.UpdateAccount(instanceID, map[string]interface{}{ - "Status": "UNPAIRED", - }) - if err != nil { - w.log.Error("Failed to update account. ", err) - } -} - -func (w *wppService) handleMessage(instanceId string, evt *events.Message) { - instance := w.app.LoadInstance(instanceId) - parsedEventMessage, err := w.ParseEventMessage(instance, evt) - - if err != nil { - w.log.Error(err) - return - } - - message := models.Message{ - SenderJID: parsedEventMessage.SenderJID, - ChatJID: parsedEventMessage.ChatJID, - InstanceID: parsedEventMessage.InstanceID, - MessageID: parsedEventMessage.MessageID, - Timestamp: parsedEventMessage.Timestamp, - Body: parsedEventMessage.Body, - FromMe: parsedEventMessage.FromMe, - } - - if parsedEventMessage.MediaType != nil { - path, err := utils.SaveMedia( - instance.ID, - parsedEventMessage.MessageID, - *parsedEventMessage.Media, - *parsedEventMessage.Mimetype, - ) - - if err != nil { - w.log.Error("Failed to save media. ", err) - } - - message.MediaType = parsedEventMessage.MediaType.String() - message.MediaPath = path - } - - err = w.messageService.CreateMessage(&message) - if err != nil { - w.log.Error("Failed to create message. ", err) - return - } - - body := map[string]interface{}{ - "InstanceId": instanceId, - "Message": w.messageService.ToJSON(message), - } - - err = utils.Request(w.app.Config.WebhookURL, body) - if err != nil { - w.log.Error("Failed to send webhook request. ", err) - } -} - -func createClient(config configs.ZapMeowConfig, deviceStore *store.Device) *whatsmeow.Client { - if config.Environment == configs.Production { - return whatsmeow.NewClient(deviceStore, nil) - } - log := waLog.Stdout("Client", "DEBUG", true) - return whatsmeow.NewClient(deviceStore, log) -} diff --git a/utils/response.go b/utils/response.go deleted file mode 100644 index 2cb2fc6..0000000 --- a/utils/response.go +++ /dev/null @@ -1,40 +0,0 @@ -package utils - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func RespondWithSuccess(c *gin.Context, data interface{}) { - c.JSON(http.StatusOK, gin.H{ - "Success": true, - "Data": data, - }) -} - -func RespondWithError(c *gin.Context, statusCode int, message string) { - c.JSON(statusCode, gin.H{ - "Success": false, - "Error": message, - }) - c.Abort() -} - -func RespondNotFound(c *gin.Context, message string) { - RespondWithError(c, http.StatusNotFound, message) -} - -func RespondBadRequest(c *gin.Context, message string) { - RespondWithError(c, http.StatusBadRequest, message) -} - -func RespondInternalServerError(c *gin.Context, message string) { - RespondWithError(c, http.StatusInternalServerError, message) -} - -func HandleError(c *gin.Context, err error) { - if err != nil { - RespondInternalServerError(c, "Internal server error") - } -} diff --git a/workers/history_sync_worker.go b/worker/history_sync_worker.go similarity index 71% rename from workers/history_sync_worker.go rename to worker/history_sync_worker.go index e418850..c52c031 100644 --- a/workers/history_sync_worker.go +++ b/worker/history_sync_worker.go @@ -1,13 +1,15 @@ -package workers +package worker import ( "sort" "time" - "zapmeow/configs" - "zapmeow/models" - "zapmeow/queues" - "zapmeow/services" - "zapmeow/utils" + "zapmeow/api/helper" + "zapmeow/api/model" + "zapmeow/api/queue" + "zapmeow/api/service" + "zapmeow/pkg/logger" + "zapmeow/pkg/whatsapp" + "zapmeow/pkg/zapmeow" waProto "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/types" @@ -16,11 +18,10 @@ import ( ) type historySyncWorker struct { - app *configs.ZapMeow - messageService services.MessageService - accountService services.AccountService - wppService services.WppService - log configs.Logger + app *zapmeow.ZapMeow + messageService service.MessageService + accountService service.AccountService + whatsAppService service.WhatsAppService } type HistorySyncWorker interface { @@ -28,24 +29,21 @@ type HistorySyncWorker interface { } func NewHistorySyncWorker( - app *configs.ZapMeow, - messageService services.MessageService, - accountService services.AccountService, - wppService services.WppService, - log configs.Logger, + app *zapmeow.ZapMeow, + messageService service.MessageService, + accountService service.AccountService, + whatsAppService service.WhatsAppService, ) *historySyncWorker { return &historySyncWorker{ - messageService: messageService, - accountService: accountService, - wppService: wppService, - app: app, - log: log, + messageService: messageService, + accountService: accountService, + whatsAppService: whatsAppService, + app: app, } } func (q *historySyncWorker) ProcessQueue() { - queue := queues.NewHistorySyncQueue(q.app, q.log) - + queue := queue.NewHistorySyncQueue(q.app) defer q.app.Wg.Done() for { select { @@ -53,7 +51,7 @@ func (q *historySyncWorker) ProcessQueue() { return default: if err := q.processHistorySync(queue); err != nil { - q.log.Error("Error processing history sync. ", err) + logger.Error("Error processing history sync. ", err) } } @@ -61,7 +59,7 @@ func (q *historySyncWorker) ProcessQueue() { } } -func (q *historySyncWorker) processHistorySync(queue queues.HistorySyncQueue) error { +func (q *historySyncWorker) processHistorySync(queue queue.HistorySyncQueue) error { data, err := queue.Dequeue() if err != nil { return err @@ -76,7 +74,7 @@ func (q *historySyncWorker) processHistorySync(queue queues.HistorySyncQueue) er return err } - instance, err := q.wppService.GetInstance(data.InstanceID) + instance, err := q.whatsAppService.GetInstance(data.InstanceID) if err != nil { return err } @@ -118,8 +116,8 @@ func (q *historySyncWorker) parseHistorySync(history []byte) (*waProto.HistorySy return &data, nil } -func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *models.Account, instance *configs.Instance) ([]models.Message, error) { - var messages []models.Message +func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *model.Account, instance *whatsapp.Instance) ([]model.Message, error) { + var messages []model.Message for _, conv := range evt.GetConversations() { chatJID, _ := types.ParseJID(conv.GetId()) @@ -147,11 +145,11 @@ func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *m return eventsMessage[i].Info.Timestamp.After(eventsMessage[j].Info.Timestamp) }) - maxMessages := utils.Min(q.app.Config.MaxMessagesPerInstance, len(eventsMessage)) + maxMessages := helper.Min(q.app.Config.MaxMessagesPerInstance, len(eventsMessage)) slice := eventsMessage[:maxMessages] for _, evtMessage := range slice { - parsedEvtMesage, err := q.wppService.ParseEventMessage(instance, evtMessage) + parsedEvtMesage, err := q.whatsAppService.ParseEventMessage(instance, evtMessage) if err != nil { continue } @@ -167,7 +165,7 @@ func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *m return messages, nil } -func (q *historySyncWorker) processConversation(conv *waProto.Conversation, chatJID types.JID, instance *configs.Instance) ([]*events.Message, error) { +func (q *historySyncWorker) processConversation(conv *waProto.Conversation, chatJID types.JID, instance *whatsapp.Instance) ([]*events.Message, error) { var eventsMessage []*events.Message for _, msg := range conv.GetMessages() { parsedMessage, err := instance.Client.ParseWebMessage(chatJID, msg.GetMessage()) @@ -179,8 +177,8 @@ func (q *historySyncWorker) processConversation(conv *waProto.Conversation, chat return eventsMessage, nil } -func (q *historySyncWorker) makeMessage(instance *configs.Instance, parsedMessage services.ParsedEventMessage) (*models.Message, error) { - message := models.Message{ +func (q *historySyncWorker) makeMessage(instance *whatsapp.Instance, parsedMessage whatsapp.Message) (*model.Message, error) { + message := model.Message{ SenderJID: parsedMessage.SenderJID, ChatJID: parsedMessage.ChatJID, InstanceID: parsedMessage.InstanceID, @@ -191,7 +189,7 @@ func (q *historySyncWorker) makeMessage(instance *configs.Instance, parsedMessag } if parsedMessage.MediaType != nil { - path, err := utils.SaveMedia( + path, err := helper.SaveMedia( instance.ID, parsedMessage.MessageID, *parsedMessage.Media, From 3028567ee9d21f23ae66a36c655ef91e8f4b035c Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 25 Dec 2023 15:01:21 -0300 Subject: [PATCH 07/16] cleanup --- api/response/response.go | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/api/response/response.go b/api/response/response.go index 1508a43..6f21c8d 100644 --- a/api/response/response.go +++ b/api/response/response.go @@ -30,36 +30,3 @@ func ErrorResponse(c *gin.Context, statusCode int, message string) { }) c.Abort() } - -// func RespondWithSuccess(c *gin.Context, data interface{}) { -// c.JSON(http.StatusOK, gin.H{ -// "Success": true, -// "Data": data, -// }) -// } - -// func RespondWithError(c *gin.Context, statusCode int, message string) { -// c.JSON(statusCode, gin.H{ -// "Success": false, -// "Error": message, -// }) -// c.Abort() -// } - -// func RespondNotFound(c *gin.Context, message string) { -// RespondWithError(c, http.StatusNotFound, message) -// } - -// func RespondBadRequest(c *gin.Context, message string) { -// RespondWithError(c, http.StatusBadRequest, message) -// } - -// func RespondInternalServerError(c *gin.Context, message string) { -// RespondWithError(c, http.StatusInternalServerError, message) -// } - -// func HandleError(c *gin.Context, err error) { -// if err != nil { -// RespondInternalServerError(c, "Internal server error") -// } -// } From 1626317f5aea3c5bf37564e2d0831071f17aabce Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 25 Dec 2023 15:01:48 -0300 Subject: [PATCH 08/16] update docs --- docs/docs.go | 207 ++++++++++++++++++++++------------------------ docs/swagger.json | 207 ++++++++++++++++++++++------------------------ docs/swagger.yaml | 171 +++++++++++++++++++------------------- 3 files changed, 286 insertions(+), 299 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index bab2b92..e06e053 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -35,13 +35,22 @@ const docTemplate = `{ "name": "instanceId", "in": "path", "required": true + }, + { + "description": "Phone", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.getMessagesBody" + } } ], "responses": { "200": { "description": "List of chat messages", "schema": { - "$ref": "#/definitions/controllers.getMessagesResponse" + "$ref": "#/definitions/handler.getMessagesResponse" } } } @@ -74,7 +83,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.audioMessageBody" + "$ref": "#/definitions/handler.sendAudioMessageBody" } } ], @@ -82,7 +91,7 @@ const docTemplate = `{ "200": { "description": "Message Send Response", "schema": { - "$ref": "#/definitions/controllers.sendAudioMessageResponse" + "$ref": "#/definitions/handler.sendAudioMessageResponse" } } } @@ -115,7 +124,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.imageMessageBody" + "$ref": "#/definitions/handler.sendImageMessageBody" } } ], @@ -123,7 +132,7 @@ const docTemplate = `{ "200": { "description": "Message Send Response", "schema": { - "$ref": "#/definitions/controllers.sendImageMessageResponse" + "$ref": "#/definitions/handler.sendImageMessageResponse" } } } @@ -156,7 +165,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.textMessageBody" + "$ref": "#/definitions/handler.sendTextMessageBody" } } ], @@ -164,7 +173,7 @@ const docTemplate = `{ "200": { "description": "Message Send Response", "schema": { - "$ref": "#/definitions/controllers.sendTextMessageResponse" + "$ref": "#/definitions/handler.sendTextMessageResponse" } } } @@ -197,7 +206,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.phoneCheckBody" + "$ref": "#/definitions/handler.getCheckPhonesBody" } } ], @@ -205,7 +214,7 @@ const docTemplate = `{ "200": { "description": "List of verified numbers", "schema": { - "$ref": "#/definitions/controllers.checkPhonesResponse" + "$ref": "#/definitions/handler.getCheckPhonesResponse" } } } @@ -244,7 +253,7 @@ const docTemplate = `{ "200": { "description": "Contact Information", "schema": { - "$ref": "#/definitions/controllers.contactInfoResponse" + "$ref": "#/definitions/handler.contactInfoResponse" } } } @@ -309,7 +318,7 @@ const docTemplate = `{ "200": { "description": "Profile Information", "schema": { - "$ref": "#/definitions/controllers.getProfileInfoResponse" + "$ref": "#/definitions/handler.getProfileInfoResponse" } } } @@ -336,9 +345,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "QR code", + "description": "QR Code", "schema": { - "$ref": "#/definitions/controllers.getQrCodeResponse" + "$ref": "#/definitions/handler.getQrCodeResponse" } } } @@ -370,7 +379,7 @@ const docTemplate = `{ "200": { "description": "Status Response", "schema": { - "$ref": "#/definitions/controllers.getStatusResponse" + "$ref": "#/definitions/handler.getStatusResponse" } } } @@ -378,64 +387,72 @@ const docTemplate = `{ } }, "definitions": { - "controllers.audioMessageBody": { + "handler.contactInfoResponse": { "type": "object", "properties": { - "base64": { - "type": "string" - }, - "phone": { - "type": "string" + "info": { + "$ref": "#/definitions/whatsapp.ContactInfo" } } }, - "controllers.checkPhonesResponse": { + "handler.getCheckPhonesBody": { "type": "object", "properties": { "phones": { "type": "array", "items": { - "$ref": "#/definitions/controllers.phone" + "type": "string" } } } }, - "controllers.contactInfoResponse": { + "handler.getCheckPhonesResponse": { "type": "object", "properties": { - "info": { - "$ref": "#/definitions/services.ContactInfo" + "phones": { + "type": "array", + "items": { + "$ref": "#/definitions/whatsapp.IsOnWhatsAppResponse" + } } } }, - "controllers.getMessagesResponse": { + "handler.getMessagesBody": { + "type": "object", + "properties": { + "phone": { + "type": "string" + } + } + }, + "handler.getMessagesResponse": { "type": "object", "properties": { "messages": { "type": "array", "items": { - "$ref": "#/definitions/services.Message" + "$ref": "#/definitions/response.Message" } } } }, - "controllers.getProfileInfoResponse": { + "handler.getProfileInfoResponse": { "type": "object", "properties": { "info": { - "$ref": "#/definitions/services.ContactInfo" + "$ref": "#/definitions/whatsapp.ContactInfo" } } }, - "controllers.getQrCodeResponse": { + "handler.getQrCodeResponse": { "type": "object", "properties": { - "qrCode": { + "qrcode": { "type": "string" } } }, - "controllers.getStatusResponse": { + "handler.getStatusResponse": { "type": "object", "properties": { "status": { @@ -443,7 +460,7 @@ const docTemplate = `{ } } }, - "controllers.imageMessageBody": { + "handler.sendAudioMessageBody": { "type": "object", "properties": { "base64": { @@ -454,84 +471,88 @@ const docTemplate = `{ } } }, - "controllers.phone": { + "handler.sendAudioMessageResponse": { "type": "object", "properties": { - "isRegistered": { - "type": "boolean" - }, - "jid": { - "type": "object", - "properties": { - "ad": { - "type": "boolean" - }, - "agent": { - "type": "integer" - }, - "device": { - "type": "integer" - }, - "server": { - "type": "string" - }, - "user": { - "type": "string" - } - } - }, - "query": { - "type": "string" + "message": { + "$ref": "#/definitions/response.Message" } } }, - "controllers.phoneCheckBody": { + "handler.sendImageMessageBody": { "type": "object", "properties": { - "phones": { - "type": "array", - "items": { - "type": "string" - } + "base64": { + "type": "string" + }, + "phone": { + "type": "string" } } }, - "controllers.sendAudioMessageResponse": { + "handler.sendImageMessageResponse": { "type": "object", "properties": { "message": { - "$ref": "#/definitions/services.Message" + "$ref": "#/definitions/response.Message" } } }, - "controllers.sendImageMessageResponse": { + "handler.sendTextMessageBody": { "type": "object", "properties": { - "message": { - "$ref": "#/definitions/services.Message" + "phone": { + "type": "string" + }, + "text": { + "type": "string" } } }, - "controllers.sendTextMessageResponse": { + "handler.sendTextMessageResponse": { "type": "object", "properties": { "message": { - "$ref": "#/definitions/services.Message" + "$ref": "#/definitions/response.Message" } } }, - "controllers.textMessageBody": { + "response.Message": { "type": "object", "properties": { - "phone": { + "body": { "type": "string" }, - "text": { + "chat": { + "type": "string" + }, + "from_me": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "media_base64": { + "type": "string" + }, + "media_mimetype": { + "type": "string" + }, + "media_type": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "sender": { + "type": "string" + }, + "timestamp": { "type": "string" } } }, - "services.ContactInfo": { + "whatsapp.ContactInfo": { "type": "object", "properties": { "name": { @@ -548,42 +569,16 @@ const docTemplate = `{ } } }, - "services.Message": { + "whatsapp.IsOnWhatsAppResponse": { "type": "object", "properties": { - "body": { - "type": "string" - }, - "chat": { - "type": "string" - }, - "fromMe": { + "is_registered": { "type": "boolean" }, - "id": { - "type": "integer" - }, - "mediaData": { - "type": "object", - "properties": { - "base64": { - "type": "string" - }, - "mimetype": { - "type": "string" - } - } - }, - "mediaType": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "sender": { + "phone": { "type": "string" }, - "timestamp": { + "query": { "type": "string" } } diff --git a/docs/swagger.json b/docs/swagger.json index e2a0fdb..890d8ac 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -29,13 +29,22 @@ "name": "instanceId", "in": "path", "required": true + }, + { + "description": "Phone", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.getMessagesBody" + } } ], "responses": { "200": { "description": "List of chat messages", "schema": { - "$ref": "#/definitions/controllers.getMessagesResponse" + "$ref": "#/definitions/handler.getMessagesResponse" } } } @@ -68,7 +77,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.audioMessageBody" + "$ref": "#/definitions/handler.sendAudioMessageBody" } } ], @@ -76,7 +85,7 @@ "200": { "description": "Message Send Response", "schema": { - "$ref": "#/definitions/controllers.sendAudioMessageResponse" + "$ref": "#/definitions/handler.sendAudioMessageResponse" } } } @@ -109,7 +118,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.imageMessageBody" + "$ref": "#/definitions/handler.sendImageMessageBody" } } ], @@ -117,7 +126,7 @@ "200": { "description": "Message Send Response", "schema": { - "$ref": "#/definitions/controllers.sendImageMessageResponse" + "$ref": "#/definitions/handler.sendImageMessageResponse" } } } @@ -150,7 +159,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.textMessageBody" + "$ref": "#/definitions/handler.sendTextMessageBody" } } ], @@ -158,7 +167,7 @@ "200": { "description": "Message Send Response", "schema": { - "$ref": "#/definitions/controllers.sendTextMessageResponse" + "$ref": "#/definitions/handler.sendTextMessageResponse" } } } @@ -191,7 +200,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.phoneCheckBody" + "$ref": "#/definitions/handler.getCheckPhonesBody" } } ], @@ -199,7 +208,7 @@ "200": { "description": "List of verified numbers", "schema": { - "$ref": "#/definitions/controllers.checkPhonesResponse" + "$ref": "#/definitions/handler.getCheckPhonesResponse" } } } @@ -238,7 +247,7 @@ "200": { "description": "Contact Information", "schema": { - "$ref": "#/definitions/controllers.contactInfoResponse" + "$ref": "#/definitions/handler.contactInfoResponse" } } } @@ -303,7 +312,7 @@ "200": { "description": "Profile Information", "schema": { - "$ref": "#/definitions/controllers.getProfileInfoResponse" + "$ref": "#/definitions/handler.getProfileInfoResponse" } } } @@ -330,9 +339,9 @@ ], "responses": { "200": { - "description": "QR code", + "description": "QR Code", "schema": { - "$ref": "#/definitions/controllers.getQrCodeResponse" + "$ref": "#/definitions/handler.getQrCodeResponse" } } } @@ -364,7 +373,7 @@ "200": { "description": "Status Response", "schema": { - "$ref": "#/definitions/controllers.getStatusResponse" + "$ref": "#/definitions/handler.getStatusResponse" } } } @@ -372,64 +381,72 @@ } }, "definitions": { - "controllers.audioMessageBody": { + "handler.contactInfoResponse": { "type": "object", "properties": { - "base64": { - "type": "string" - }, - "phone": { - "type": "string" + "info": { + "$ref": "#/definitions/whatsapp.ContactInfo" } } }, - "controllers.checkPhonesResponse": { + "handler.getCheckPhonesBody": { "type": "object", "properties": { "phones": { "type": "array", "items": { - "$ref": "#/definitions/controllers.phone" + "type": "string" } } } }, - "controllers.contactInfoResponse": { + "handler.getCheckPhonesResponse": { "type": "object", "properties": { - "info": { - "$ref": "#/definitions/services.ContactInfo" + "phones": { + "type": "array", + "items": { + "$ref": "#/definitions/whatsapp.IsOnWhatsAppResponse" + } } } }, - "controllers.getMessagesResponse": { + "handler.getMessagesBody": { + "type": "object", + "properties": { + "phone": { + "type": "string" + } + } + }, + "handler.getMessagesResponse": { "type": "object", "properties": { "messages": { "type": "array", "items": { - "$ref": "#/definitions/services.Message" + "$ref": "#/definitions/response.Message" } } } }, - "controllers.getProfileInfoResponse": { + "handler.getProfileInfoResponse": { "type": "object", "properties": { "info": { - "$ref": "#/definitions/services.ContactInfo" + "$ref": "#/definitions/whatsapp.ContactInfo" } } }, - "controllers.getQrCodeResponse": { + "handler.getQrCodeResponse": { "type": "object", "properties": { - "qrCode": { + "qrcode": { "type": "string" } } }, - "controllers.getStatusResponse": { + "handler.getStatusResponse": { "type": "object", "properties": { "status": { @@ -437,7 +454,7 @@ } } }, - "controllers.imageMessageBody": { + "handler.sendAudioMessageBody": { "type": "object", "properties": { "base64": { @@ -448,84 +465,88 @@ } } }, - "controllers.phone": { + "handler.sendAudioMessageResponse": { "type": "object", "properties": { - "isRegistered": { - "type": "boolean" - }, - "jid": { - "type": "object", - "properties": { - "ad": { - "type": "boolean" - }, - "agent": { - "type": "integer" - }, - "device": { - "type": "integer" - }, - "server": { - "type": "string" - }, - "user": { - "type": "string" - } - } - }, - "query": { - "type": "string" + "message": { + "$ref": "#/definitions/response.Message" } } }, - "controllers.phoneCheckBody": { + "handler.sendImageMessageBody": { "type": "object", "properties": { - "phones": { - "type": "array", - "items": { - "type": "string" - } + "base64": { + "type": "string" + }, + "phone": { + "type": "string" } } }, - "controllers.sendAudioMessageResponse": { + "handler.sendImageMessageResponse": { "type": "object", "properties": { "message": { - "$ref": "#/definitions/services.Message" + "$ref": "#/definitions/response.Message" } } }, - "controllers.sendImageMessageResponse": { + "handler.sendTextMessageBody": { "type": "object", "properties": { - "message": { - "$ref": "#/definitions/services.Message" + "phone": { + "type": "string" + }, + "text": { + "type": "string" } } }, - "controllers.sendTextMessageResponse": { + "handler.sendTextMessageResponse": { "type": "object", "properties": { "message": { - "$ref": "#/definitions/services.Message" + "$ref": "#/definitions/response.Message" } } }, - "controllers.textMessageBody": { + "response.Message": { "type": "object", "properties": { - "phone": { + "body": { "type": "string" }, - "text": { + "chat": { + "type": "string" + }, + "from_me": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "media_base64": { + "type": "string" + }, + "media_mimetype": { + "type": "string" + }, + "media_type": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "sender": { + "type": "string" + }, + "timestamp": { "type": "string" } } }, - "services.ContactInfo": { + "whatsapp.ContactInfo": { "type": "object", "properties": { "name": { @@ -542,42 +563,16 @@ } } }, - "services.Message": { + "whatsapp.IsOnWhatsAppResponse": { "type": "object", "properties": { - "body": { - "type": "string" - }, - "chat": { - "type": "string" - }, - "fromMe": { + "is_registered": { "type": "boolean" }, - "id": { - "type": "integer" - }, - "mediaData": { - "type": "object", - "properties": { - "base64": { - "type": "string" - }, - "mimetype": { - "type": "string" - } - } - }, - "mediaType": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "sender": { + "phone": { "type": "string" }, - "timestamp": { + "query": { "type": "string" } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6579ade..0633f60 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,139 +1,130 @@ basePath: /api definitions: - controllers.audioMessageBody: + handler.contactInfoResponse: properties: - base64: - type: string - phone: - type: string + info: + $ref: '#/definitions/whatsapp.ContactInfo' type: object - controllers.checkPhonesResponse: + handler.getCheckPhonesBody: properties: phones: items: - $ref: '#/definitions/controllers.phone' + type: string type: array type: object - controllers.contactInfoResponse: + handler.getCheckPhonesResponse: properties: - info: - $ref: '#/definitions/services.ContactInfo' + phones: + items: + $ref: '#/definitions/whatsapp.IsOnWhatsAppResponse' + type: array + type: object + handler.getMessagesBody: + properties: + phone: + type: string type: object - controllers.getMessagesResponse: + handler.getMessagesResponse: properties: messages: items: - $ref: '#/definitions/services.Message' + $ref: '#/definitions/response.Message' type: array type: object - controllers.getProfileInfoResponse: + handler.getProfileInfoResponse: properties: info: - $ref: '#/definitions/services.ContactInfo' + $ref: '#/definitions/whatsapp.ContactInfo' type: object - controllers.getQrCodeResponse: + handler.getQrCodeResponse: properties: - qrCode: + qrcode: type: string type: object - controllers.getStatusResponse: + handler.getStatusResponse: properties: status: type: string type: object - controllers.imageMessageBody: + handler.sendAudioMessageBody: properties: base64: type: string phone: type: string type: object - controllers.phone: - properties: - isRegistered: - type: boolean - jid: - properties: - ad: - type: boolean - agent: - type: integer - device: - type: integer - server: - type: string - user: - type: string - type: object - query: - type: string - type: object - controllers.phoneCheckBody: - properties: - phones: - items: - type: string - type: array - type: object - controllers.sendAudioMessageResponse: + handler.sendAudioMessageResponse: properties: message: - $ref: '#/definitions/services.Message' + $ref: '#/definitions/response.Message' type: object - controllers.sendImageMessageResponse: + handler.sendImageMessageBody: properties: - message: - $ref: '#/definitions/services.Message' + base64: + type: string + phone: + type: string type: object - controllers.sendTextMessageResponse: + handler.sendImageMessageResponse: properties: message: - $ref: '#/definitions/services.Message' + $ref: '#/definitions/response.Message' type: object - controllers.textMessageBody: + handler.sendTextMessageBody: properties: phone: type: string text: type: string type: object - services.ContactInfo: + handler.sendTextMessageResponse: properties: - name: - type: string - phone: - type: string - picture: - type: string - status: - type: string + message: + $ref: '#/definitions/response.Message' type: object - services.Message: + response.Message: properties: body: type: string chat: type: string - fromMe: + from_me: type: boolean id: type: integer - mediaData: - properties: - base64: - type: string - mimetype: - type: string - type: object - mediaType: + media_base64: + type: string + media_mimetype: type: string - messageID: + media_type: + type: string + message_id: type: string sender: type: string timestamp: type: string type: object + whatsapp.ContactInfo: + properties: + name: + type: string + phone: + type: string + picture: + type: string + status: + type: string + type: object + whatsapp.IsOnWhatsAppResponse: + properties: + is_registered: + type: boolean + phone: + type: string + query: + type: string + type: object host: localhost:8900 info: contact: {} @@ -152,13 +143,19 @@ paths: name: instanceId required: true type: string + - description: Phone + in: body + name: data + required: true + schema: + $ref: '#/definitions/handler.getMessagesBody' produces: - application/json responses: "200": description: List of chat messages schema: - $ref: '#/definitions/controllers.getMessagesResponse' + $ref: '#/definitions/handler.getMessagesResponse' summary: Get WhatsApp Chat Messages tags: - WhatsApp Chat @@ -178,14 +175,14 @@ paths: name: data required: true schema: - $ref: '#/definitions/controllers.audioMessageBody' + $ref: '#/definitions/handler.sendAudioMessageBody' produces: - application/json responses: "200": description: Message Send Response schema: - $ref: '#/definitions/controllers.sendAudioMessageResponse' + $ref: '#/definitions/handler.sendAudioMessageResponse' summary: Send Audio Message on WhatsApp tags: - WhatsApp Chat @@ -205,14 +202,14 @@ paths: name: data required: true schema: - $ref: '#/definitions/controllers.imageMessageBody' + $ref: '#/definitions/handler.sendImageMessageBody' produces: - application/json responses: "200": description: Message Send Response schema: - $ref: '#/definitions/controllers.sendImageMessageResponse' + $ref: '#/definitions/handler.sendImageMessageResponse' summary: Send Image Message on WhatsApp tags: - WhatsApp Chat @@ -232,14 +229,14 @@ paths: name: data required: true schema: - $ref: '#/definitions/controllers.textMessageBody' + $ref: '#/definitions/handler.sendTextMessageBody' produces: - application/json responses: "200": description: Message Send Response schema: - $ref: '#/definitions/controllers.sendTextMessageResponse' + $ref: '#/definitions/handler.sendTextMessageResponse' summary: Send Text Message on WhatsApp tags: - WhatsApp Chat @@ -260,14 +257,14 @@ paths: name: data required: true schema: - $ref: '#/definitions/controllers.phoneCheckBody' + $ref: '#/definitions/handler.getCheckPhonesBody' produces: - application/json responses: "200": description: List of verified numbers schema: - $ref: '#/definitions/controllers.checkPhonesResponse' + $ref: '#/definitions/handler.getCheckPhonesResponse' summary: Check Phones on WhatsApp tags: - WhatsApp Phone Verification @@ -293,7 +290,7 @@ paths: "200": description: Contact Information schema: - $ref: '#/definitions/controllers.contactInfoResponse' + $ref: '#/definitions/handler.contactInfoResponse' summary: Get Contact Information tags: - WhatsApp Contact @@ -336,7 +333,7 @@ paths: "200": description: Profile Information schema: - $ref: '#/definitions/controllers.getProfileInfoResponse' + $ref: '#/definitions/handler.getProfileInfoResponse' summary: Get Profile Information tags: - WhatsApp Profile @@ -353,9 +350,9 @@ paths: - application/json responses: "200": - description: QR code + description: QR Code schema: - $ref: '#/definitions/controllers.getQrCodeResponse' + $ref: '#/definitions/handler.getQrCodeResponse' summary: Get WhatsApp QR Code tags: - WhatsApp Login @@ -376,7 +373,7 @@ paths: "200": description: Status Response schema: - $ref: '#/definitions/controllers.getStatusResponse' + $ref: '#/definitions/handler.getStatusResponse' summary: Get WhatsApp Instance Status tags: - WhatsApp Status From e3c3e34f298d151c16a91122e91975bae0da531f Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Thu, 28 Dec 2023 17:57:24 -0300 Subject: [PATCH 09/16] lint --- api/handler/check_phones_handler.go | 19 ++++++++++--------- api/handler/get_contract_info_handler.go | 19 ++++++++++--------- api/handler/get_messages_handler.go | 19 ++++++++++--------- api/handler/get_profile_info_handler.go | 17 +++++++++-------- api/handler/get_qrcode_handler.go | 15 ++++++++------- api/handler/get_status_handler.go | 17 +++++++++-------- api/handler/logout_handler.go | 17 +++++++++-------- api/handler/send_audio_message_handler.go | 19 ++++++++++--------- api/handler/send_image_message_handler.go | 19 ++++++++++--------- api/handler/send_text_message_handler.go | 19 ++++++++++--------- cmd/server/main.go | 10 +++++----- 11 files changed, 100 insertions(+), 90 deletions(-) diff --git a/api/handler/check_phones_handler.go b/api/handler/check_phones_handler.go index d38fa0a..d139418 100644 --- a/api/handler/check_phones_handler.go +++ b/api/handler/check_phones_handler.go @@ -30,15 +30,16 @@ func NewCheckPhonesHandler( } // Check Phones on WhatsApp -// @Summary Check Phones on WhatsApp -// @Description Verifies if the phone numbers in the provided list are registered WhatsApp users. -// @Tags WhatsApp Phone Verification -// @Param instanceId path string true "Instance ID" -// @Param data body getCheckPhonesBody true "Phone list" -// @Accept json -// @Produce json -// @Success 200 {object} getCheckPhonesResponse "List of verified numbers" -// @Router /{instanceId}/check/phones [post] +// +// @Summary Check Phones on WhatsApp +// @Description Verifies if the phone numbers in the provided list are registered WhatsApp users. +// @Tags WhatsApp Phone Verification +// @Param instanceId path string true "Instance ID" +// @Param data body getCheckPhonesBody true "Phone list" +// @Accept json +// @Produce json +// @Success 200 {object} getCheckPhonesResponse "List of verified numbers" +// @Router /{instanceId}/check/phones [post] func (h *checkPhonesHandler) Handler(c *gin.Context) { var body getCheckPhonesBody if err := c.ShouldBindJSON(&body); err != nil { diff --git a/api/handler/get_contract_info_handler.go b/api/handler/get_contract_info_handler.go index eec2fcc..4a63d41 100644 --- a/api/handler/get_contract_info_handler.go +++ b/api/handler/get_contract_info_handler.go @@ -27,15 +27,16 @@ func NewGetContactInfoHandler( } // Get Contact Information -// @Summary Get Contact Information -// @Description Retrieves contact information. -// @Tags WhatsApp Contact -// @Param instanceId path string true "Instance ID" -// @Param phone query string true "Phone" -// @Accept json -// @Produce json -// @Success 200 {object} contactInfoResponse "Contact Information" -// @Router /{instanceId}/contact/info [get] +// +// @Summary Get Contact Information +// @Description Retrieves contact information. +// @Tags WhatsApp Contact +// @Param instanceId path string true "Instance ID" +// @Param phone query string true "Phone" +// @Accept json +// @Produce json +// @Success 200 {object} contactInfoResponse "Contact Information" +// @Router /{instanceId}/contact/info [get] func (h *getContactInfoHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/get_messages_handler.go b/api/handler/get_messages_handler.go index dbbbac4..1ec648b 100644 --- a/api/handler/get_messages_handler.go +++ b/api/handler/get_messages_handler.go @@ -32,15 +32,16 @@ func NewGetMessagesHandler( } // Get WhatsApp Chat Messages -// @Summary Get WhatsApp Chat Messages -// @Description Returns chat messages from the specified WhatsApp instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body getMessagesBody true "Phone" -// @Accept json -// @Produce json -// @Success 200 {object} getMessagesResponse "List of chat messages" -// @Router /{instanceId}/chat/messages [post] +// +// @Summary Get WhatsApp Chat Messages +// @Description Returns chat messages from the specified WhatsApp instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body getMessagesBody true "Phone" +// @Accept json +// @Produce json +// @Success 200 {object} getMessagesResponse "List of chat messages" +// @Router /{instanceId}/chat/messages [post] func (h *getMessagesHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/get_profile_info_handler.go b/api/handler/get_profile_info_handler.go index 3b7039f..8442e50 100644 --- a/api/handler/get_profile_info_handler.go +++ b/api/handler/get_profile_info_handler.go @@ -27,14 +27,15 @@ func NewGetProfileInfoHandler( } // Get Profile Information -// @Summary Get Profile Information -// @Description Retrieves profile information. -// @Tags WhatsApp Profile -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} getProfileInfoResponse "Profile Information" -// @Router /{instanceId}/profile [get] +// +// @Summary Get Profile Information +// @Description Retrieves profile information. +// @Tags WhatsApp Profile +// @Param instanceId path string true "Instance ID" +// @Accept json +// @Produce json +// @Success 200 {object} getProfileInfoResponse "Profile Information" +// @Router /{instanceId}/profile [get] func (h *getProfileInfoHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/get_qrcode_handler.go b/api/handler/get_qrcode_handler.go index 6a1fa57..3bb43b6 100644 --- a/api/handler/get_qrcode_handler.go +++ b/api/handler/get_qrcode_handler.go @@ -35,13 +35,14 @@ func NewGetQrCodeHandler( } // Get QR Code for WhatsApp Login -// @Summary Get WhatsApp QR Code -// @Description Returns a QR code to initiate WhatsApp login. -// @Tags WhatsApp Login -// @Param instanceId path string true "Instance ID" -// @Produce json -// @Success 200 {object} getQrCodeResponse "QR Code" -// @Router /{instanceId}/qrcode [get] +// +// @Summary Get WhatsApp QR Code +// @Description Returns a QR code to initiate WhatsApp login. +// @Tags WhatsApp Login +// @Param instanceId path string true "Instance ID" +// @Produce json +// @Success 200 {object} getQrCodeResponse "QR Code" +// @Router /{instanceId}/qrcode [get] func (h *getQrCodeHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") _, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/get_status_handler.go b/api/handler/get_status_handler.go index 97e060d..3efdbba 100644 --- a/api/handler/get_status_handler.go +++ b/api/handler/get_status_handler.go @@ -32,14 +32,15 @@ func NewGetStatusHandler( } // Get WhatsApp Instance Status -// @Summary Get WhatsApp Instance Status -// @Description Returns the status of the specified WhatsApp instance. -// @Tags WhatsApp Status -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} getStatusResponse "Status Response" -// @Router /{instanceId}/status [get] +// +// @Summary Get WhatsApp Instance Status +// @Description Returns the status of the specified WhatsApp instance. +// @Tags WhatsApp Status +// @Param instanceId path string true "Instance ID" +// @Accept json +// @Produce json +// @Success 200 {object} getStatusResponse "Status Response" +// @Router /{instanceId}/status [get] func (h *getStatusHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/logout_handler.go b/api/handler/logout_handler.go index 8a35450..5897c67 100644 --- a/api/handler/logout_handler.go +++ b/api/handler/logout_handler.go @@ -28,14 +28,15 @@ func NewLogoutHandler( } // Logout from WhatsApp -// @Summary Logout from WhatsApp -// @Description Logs out from the specified WhatsApp instance. -// @Tags WhatsApp Logout -// @Param instanceId path string true "Instance ID" -// @Accept json -// @Produce json -// @Success 200 {object} map[string]interface{} "Logout successful" -// @Router /{instanceId}/logout [post] +// +// @Summary Logout from WhatsApp +// @Description Logs out from the specified WhatsApp instance. +// @Tags WhatsApp Logout +// @Param instanceId path string true "Instance ID" +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "Logout successful" +// @Router /{instanceId}/logout [post] func (h *logoutHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/send_audio_message_handler.go b/api/handler/send_audio_message_handler.go index a148b87..11778df 100644 --- a/api/handler/send_audio_message_handler.go +++ b/api/handler/send_audio_message_handler.go @@ -36,15 +36,16 @@ func NewSendAudioMessageHandler( } // Send Audio Message on WhatsApp -// @Summary Send Audio Message on WhatsApp -// @Description Sends an audio message on WhatsApp using the specified instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body sendAudioMessageBody true "Audio message body" -// @Accept json -// @Produce json -// @Success 200 {object} sendAudioMessageResponse "Message Send Response" -// @Router /{instanceId}/chat/send/audio [post] +// +// @Summary Send Audio Message on WhatsApp +// @Description Sends an audio message on WhatsApp using the specified instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body sendAudioMessageBody true "Audio message body" +// @Accept json +// @Produce json +// @Success 200 {object} sendAudioMessageResponse "Message Send Response" +// @Router /{instanceId}/chat/send/audio [post] func (h *sendAudioMessageHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/send_image_message_handler.go b/api/handler/send_image_message_handler.go index 30e048a..3a4c73d 100644 --- a/api/handler/send_image_message_handler.go +++ b/api/handler/send_image_message_handler.go @@ -36,15 +36,16 @@ func NewSendImageMessageHandler( } // Send Image Message on WhatsApp -// @Summary Send Image Message on WhatsApp -// @Description Sends an image message on WhatsApp using the specified instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body sendImageMessageBody true "Image message body" -// @Accept json -// @Produce json -// @Success 200 {object} sendImageMessageResponse "Message Send Response" -// @Router /{instanceId}/chat/send/image [post] +// +// @Summary Send Image Message on WhatsApp +// @Description Sends an image message on WhatsApp using the specified instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body sendImageMessageBody true "Image message body" +// @Accept json +// @Produce json +// @Success 200 {object} sendImageMessageResponse "Message Send Response" +// @Router /{instanceId}/chat/send/image [post] func (h *sendImageMessageHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/api/handler/send_text_message_handler.go b/api/handler/send_text_message_handler.go index bd04c3d..8f585c9 100644 --- a/api/handler/send_text_message_handler.go +++ b/api/handler/send_text_message_handler.go @@ -35,15 +35,16 @@ func NewSendTextMessageHandler( } // Send Text Message on WhatsApp -// @Summary Send Text Message on WhatsApp -// @Description Sends a text message on WhatsApp using the specified instance. -// @Tags WhatsApp Chat -// @Param instanceId path string true "Instance ID" -// @Param data body sendTextMessageBody true "Text message body" -// @Accept json -// @Produce json -// @Success 200 {object} sendTextMessageResponse "Message Send Response" -// @Router /{instanceId}/chat/send/text [post] +// +// @Summary Send Text Message on WhatsApp +// @Description Sends a text message on WhatsApp using the specified instance. +// @Tags WhatsApp Chat +// @Param instanceId path string true "Instance ID" +// @Param data body sendTextMessageBody true "Text message body" +// @Accept json +// @Produce json +// @Success 200 {object} sendTextMessageResponse "Message Send Response" +// @Router /{instanceId}/chat/send/text [post] func (h *sendTextMessageHandler) Handler(c *gin.Context) { instanceID := c.Param("instanceId") instance, err := h.whatsAppService.GetInstance(instanceID) diff --git a/cmd/server/main.go b/cmd/server/main.go index 75dad7e..08dcaab 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -17,11 +17,11 @@ import ( "github.com/joho/godotenv" ) -// @title ZapMeow API -// @version 1.0 -// @description API to handle multiple WhatsApp instances -// @host localhost:8900 -// @BasePath /api +// @title ZapMeow API +// @version 1.0 +// @description API to handle multiple WhatsApp instances +// @host localhost:8900 +// @BasePath /api func main() { logger.Init() From dad9a785bfe9aeaeece5f3bd76896c109f68ed69 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Thu, 28 Dec 2023 19:52:16 -0300 Subject: [PATCH 10/16] check if the folder exists before deleting --- api/service/account_service.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/service/account_service.go b/api/service/account_service.go index 9d9986f..cc12410 100644 --- a/api/service/account_service.go +++ b/api/service/account_service.go @@ -54,7 +54,12 @@ func (a *accountService) DeleteAccountMessages(instanceID string) error { func (a *accountService) deleteAccountDirectory(instanceID string) error { dirPath := helper.MakeAccountStoragePath(instanceID) - err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + _, err := os.Stat(dirPath) + if err != nil { + return nil + } + + err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } From d3cfb742c76157bf1ee21d12588cb5568f589598 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Thu, 28 Dec 2023 19:56:54 -0300 Subject: [PATCH 11/16] fix --- pkg/queue/redis.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/queue/redis.go b/pkg/queue/redis.go index 4b90d92..458bb8b 100644 --- a/pkg/queue/redis.go +++ b/pkg/queue/redis.go @@ -7,7 +7,7 @@ import ( ) type Queue interface { - Enqueue(queueName string, values ...interface{}) error + Enqueue(queueName string, data []byte) error Dequeue(queueName string) ([]byte, error) } @@ -29,8 +29,8 @@ func NewQueue(addr string, password string) *queue { } } -func (q *queue) Enqueue(queueName string, values ...interface{}) error { - return q.client.LPush(queueName, values).Err() +func (q *queue) Enqueue(queueName string, data []byte) error { + return q.client.LPush(queueName, data).Err() } func (q *queue) Dequeue(queueName string) ([]byte, error) { From b115c38415e52142fddf8602eb49c84955c540c0 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 1 Jan 2024 14:59:12 -0300 Subject: [PATCH 12/16] config gin release mode in production environment --- api/route/routes.go | 14 +------------- cmd/server/main.go | 8 ++++++++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/api/route/routes.go b/api/route/routes.go index 41fc80f..76c7177 100644 --- a/api/route/routes.go +++ b/api/route/routes.go @@ -3,32 +3,20 @@ package route import ( "zapmeow/api/handler" "zapmeow/api/service" - "zapmeow/config" "zapmeow/pkg/zapmeow" - docs "zapmeow/docs" - "github.com/gin-gonic/gin" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) -func makeEngine(app *zapmeow.ZapMeow) *gin.Engine { - if app.Config.Environment == config.Development { - return gin.New() - } - return gin.Default() -} - func SetupRouter( app *zapmeow.ZapMeow, whatsAppService service.WhatsAppService, messageService service.MessageService, accountService service.AccountService, ) *gin.Engine { - docs.SwaggerInfo.BasePath = "/api" - - router := makeEngine(app) + router := gin.Default() getQrCodeHandler := handler.NewGetQrCodeHandler( app, diff --git a/cmd/server/main.go b/cmd/server/main.go index 08dcaab..3486b9a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -7,6 +7,7 @@ import ( "zapmeow/api/route" "zapmeow/api/service" "zapmeow/config" + "zapmeow/docs" "zapmeow/pkg/database" "zapmeow/pkg/logger" "zapmeow/pkg/queue" @@ -14,6 +15,7 @@ import ( "zapmeow/pkg/zapmeow" "zapmeow/worker" + "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) @@ -23,6 +25,8 @@ import ( // @host localhost:8900 // @BasePath /api func main() { + docs.SwaggerInfo.BasePath = "/api" + logger.Init() err := godotenv.Load() @@ -31,6 +35,10 @@ func main() { } cfg := config.Load() + if cfg.Environment == config.Production { + gin.SetMode(gin.ReleaseMode) + } + var instances sync.Map // whatsmeow instances var mutex sync.Mutex var wg sync.WaitGroup From 76c8d81097fdde147c4909b6eb7f148c5a52a1da Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 1 Jan 2024 15:45:09 -0300 Subject: [PATCH 13/16] add script to run the project --- README.md | 58 +++++++++++++++++++++++++++---------------------------- zapmeow | 3 +++ 2 files changed, 32 insertions(+), 29 deletions(-) create mode 100755 zapmeow diff --git a/README.md b/README.md index caf6af6..6f7f0c7 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ ZapMeow is a versatile API that allows developers to interact with WhatsApp usin ### Features -- **Multi-Instance Support**: Seamlessly manage and interact with multiple WhatsApp instances concurrently. -- **Message Sending**: Send text, image, and audio messages to WhatsApp contacts and groups. -- **Phone Number Verification**: Check if phone numbers are registered on WhatsApp. -- **Contact Information**: Obtain contact information. -- **Profile Information**: Obtain profile information. -- **QR Code Generation**: Generate QR codes to initiate WhatsApp login. -- **Instance Status**: Retrieve the connection status of a specific instance of WhatsApp. +- **Multi-Instance Support**: Seamlessly manage and interact with multiple WhatsApp instances concurrently. +- **Message Sending**: Send text, image, and audio messages to WhatsApp contacts and groups. +- **Phone Number Verification**: Check if phone numbers are registered on WhatsApp. +- **Contact Information**: Obtain contact information. +- **Profile Information**: Obtain profile information. +- **QR Code Generation**: Generate QR codes to initiate WhatsApp login. +- **Instance Status**: Retrieve the connection status of a specific instance of WhatsApp. ### Getting Started @@ -18,44 +18,44 @@ To get started with the ZapMeow API, follow these simple steps: 1. **Clone the Repository**: Clone this repository to your local machine using the following command: - ```sh - git clone git@github.com:capsulbrasil/zapmeow.git - ``` + ```sh + git clone git@github.com:capsulbrasil/zapmeow.git + ``` 2. **Configuration**: Set up your project configuration by copying the provided `.env.example` file and updating the environment variables. - - Navigate to the project directory: + - Navigate to the project directory: - ```sh - cd zapmeow - ``` + ```sh + cd zapmeow + ``` - - Create a copy of the `.env.example` file as `.env`: + - Create a copy of the `.env.example` file as `.env`: - ```sh - cp .env.example .env - ``` + ```sh + cp .env.example .env + ``` - - Open the `.env` file using your preferred text editor and update the necessary environment variables. + - Open the `.env` file using your preferred text editor and update the necessary environment variables. 3. **Install Dependencies**: Install the project dependencies using the following command: - ```sh - go mod tidy - ``` + ```sh + go mod tidy + ``` 4. **Start the API**: Run the API server by executing the following command: - ```sh - go run main.go - ``` + ```sh + ./zapmeow + ``` 5. **Access Swagger Documentation**: You can access the Swagger documentation by visiting the following URL in your web browser: - ``` - http://localhost:8900/api/swagger/index.html - ``` + ``` + http://localhost:8900/api/swagger/index.html + ``` - The Swagger documentation provides detailed information about the available API endpoints, request parameters, and response formats. + The Swagger documentation provides detailed information about the available API endpoints, request parameters, and response formats. Now, your ZapMeow API is up and running, ready for you to start interacting with WhatsApp instances programmatically. diff --git a/zapmeow b/zapmeow new file mode 100755 index 0000000..9cb30dd --- /dev/null +++ b/zapmeow @@ -0,0 +1,3 @@ +#!/bin/bash + +go run cmd/server/main.go From 5083ceb3c9ad7da5c2e1274e07660c3f83dce537 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 1 Jan 2024 16:06:04 -0300 Subject: [PATCH 14/16] configs log level in production environment --- api/route/routes.go | 10 +++++++++- cmd/server/main.go | 8 +++++--- config/config.go | 8 +++++--- pkg/logger/logger.go | 6 ++++++ pkg/whatsapp/whatsapp.go | 22 +++++++++++++++++----- 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/api/route/routes.go b/api/route/routes.go index 76c7177..ef577ef 100644 --- a/api/route/routes.go +++ b/api/route/routes.go @@ -3,6 +3,7 @@ package route import ( "zapmeow/api/handler" "zapmeow/api/service" + "zapmeow/config" "zapmeow/pkg/zapmeow" "github.com/gin-gonic/gin" @@ -10,13 +11,20 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) +func makeEngine(cfg config.Config) *gin.Engine { + if cfg.Environment == config.Production { + return gin.New() + } + return gin.Default() +} + func SetupRouter( app *zapmeow.ZapMeow, whatsAppService service.WhatsAppService, messageService service.MessageService, accountService service.AccountService, ) *gin.Engine { - router := gin.Default() + router := makeEngine(app.Config) getQrCodeHandler := handler.NewGetQrCodeHandler( app, diff --git a/cmd/server/main.go b/cmd/server/main.go index 3486b9a..7e0d524 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "sync" "zapmeow/api/model" "zapmeow/api/repository" @@ -27,18 +28,18 @@ import ( func main() { docs.SwaggerInfo.BasePath = "/api" - logger.Init() - err := godotenv.Load() if err != nil { logger.Fatal("Error loading dotfile. ", err) } - cfg := config.Load() + cfg := config.Load() if cfg.Environment == config.Production { gin.SetMode(gin.ReleaseMode) } + logger.Init() + var instances sync.Map // whatsmeow instances var mutex sync.Mutex var wg sync.WaitGroup @@ -111,6 +112,7 @@ func main() { } go func() { + fmt.Println("Server is running") if err := r.Run(cfg.Port); err != nil { logger.Fatal(err) } diff --git a/config/config.go b/config/config.go index 42ce680..8793dd7 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,8 @@ package config -import "os" +import ( + "os" +) type Environment = uint @@ -28,10 +30,10 @@ func Load() Config { redisAddr := os.Getenv("REDIS_ADDR") redisPassword := os.Getenv("REDIS_PASSWORD") port := os.Getenv("PORT") - env := getEnvironment() + environment := getEnvironment() return Config{ - Environment: env, + Environment: environment, StoragePath: storagePath, WebhookURL: webhookURL, DatabaseURL: databaseURL, diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index ae62d37..1fd53d6 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -2,6 +2,7 @@ package logger import ( "os" + "zapmeow/config" "github.com/sirupsen/logrus" ) @@ -11,6 +12,7 @@ type Fields = logrus.Fields var log *logrus.Logger func Init() { + cfg := config.Load() log = logrus.New() log.SetFormatter(&logrus.TextFormatter{ @@ -18,6 +20,10 @@ func Init() { FullTimestamp: true, }) + if cfg.Environment == config.Production { + log.SetLevel(logrus.ErrorLevel) + } + log.SetOutput(os.Stdout) } diff --git a/pkg/whatsapp/whatsapp.go b/pkg/whatsapp/whatsapp.go index eafee1f..757bc8e 100644 --- a/pkg/whatsapp/whatsapp.go +++ b/pkg/whatsapp/whatsapp.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "time" + "zapmeow/config" "zapmeow/pkg/logger" "github.com/vincent-petithory/dataurl" @@ -124,7 +125,14 @@ type whatsApp struct { } func NewWhatsApp(databasePath string) *whatsApp { - dbLog := waLog.Stdout("Database", "DEBUG", true) + cfg := config.Load() + + var level = "DEBUG" + if cfg.Environment == config.Production { + level = "ERROR" + } + dbLog := waLog.Stdout("Database", level, true) + container, err := sqlstore.New("sqlite3", "file:"+databasePath+"?_foreign_keys=on", dbLog) if err != nil { logger.Fatal(err) @@ -341,10 +349,14 @@ func (w *whatsApp) ParseEventMessage(instance *Instance, message *events.Message } func (w *whatsApp) createClient(deviceStore *store.Device) *whatsmeow.Client { - // TODO: verificar se o ambiente é de produção ou dev - log := waLog.Stdout("Client", "DEBUG", true) - client := whatsmeow.NewClient(deviceStore, log) - return client + cfg := config.Load() + + var level = "DEBUG" + if cfg.Environment == config.Production { + level = "ERROR" + } + log := waLog.Stdout("Client", level, true) + return whatsmeow.NewClient(deviceStore, log) } func (w *whatsApp) uploadMedia(instance *Instance, media *dataurl.DataURL, mediaType MediaType) (*UploadResponse, error) { From b1fb3df2c3b74300c1a05c04b6585a86cd16d755 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 1 Jan 2024 16:14:15 -0300 Subject: [PATCH 15/16] rename --- config/config.go | 4 ++-- worker/history_sync_worker.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 8793dd7..bba81a3 100644 --- a/config/config.go +++ b/config/config.go @@ -20,7 +20,7 @@ type Config struct { RedisPassword string Port string QueueName string - MaxMessagesPerInstance int + MaxMessagesForChatSync int } func Load() Config { @@ -41,7 +41,7 @@ func Load() Config { RedisPassword: redisPassword, Port: port, QueueName: "queue:history-sync", - MaxMessagesPerInstance: 10, + MaxMessagesForChatSync: 10, } } diff --git a/worker/history_sync_worker.go b/worker/history_sync_worker.go index c52c031..774bf14 100644 --- a/worker/history_sync_worker.go +++ b/worker/history_sync_worker.go @@ -127,7 +127,7 @@ func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *m return nil, err } - if count > int64(q.app.Config.MaxMessagesPerInstance) { + if count > int64(q.app.Config.MaxMessagesForChatSync) { continue } @@ -145,7 +145,7 @@ func (q *historySyncWorker) processMessages(evt *waProto.HistorySync, account *m return eventsMessage[i].Info.Timestamp.After(eventsMessage[j].Info.Timestamp) }) - maxMessages := helper.Min(q.app.Config.MaxMessagesPerInstance, len(eventsMessage)) + maxMessages := helper.Min(q.app.Config.MaxMessagesForChatSync, len(eventsMessage)) slice := eventsMessage[:maxMessages] for _, evtMessage := range slice { From 84fad5916a3dc8bf30f5bb7229418fe97353b7a8 Mon Sep 17 00:00:00 2001 From: Gabriel Augusto Date: Mon, 1 Jan 2024 16:15:56 -0300 Subject: [PATCH 16/16] rename --- api/queue/history_sync_queue.go | 4 ++-- config/config.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/queue/history_sync_queue.go b/api/queue/history_sync_queue.go index ce74568..11631a4 100644 --- a/api/queue/history_sync_queue.go +++ b/api/queue/history_sync_queue.go @@ -35,11 +35,11 @@ func (q *historySyncQueue) Enqueue(item HistorySyncQueueData) error { return err } - return q.app.Queue.Enqueue(q.app.Config.QueueName, jsonData) + return q.app.Queue.Enqueue(q.app.Config.HistorySyncQueueName, jsonData) } func (q *historySyncQueue) Dequeue() (*HistorySyncQueueData, error) { - result, err := q.app.Queue.Dequeue(q.app.Config.QueueName) + result, err := q.app.Queue.Dequeue(q.app.Config.HistorySyncQueueName) if err != nil { logger.Error("Error dequeuing history sync", logger.Fields{ "error": err, diff --git a/config/config.go b/config/config.go index bba81a3..746f88d 100644 --- a/config/config.go +++ b/config/config.go @@ -19,7 +19,7 @@ type Config struct { RedisAddr string RedisPassword string Port string - QueueName string + HistorySyncQueueName string MaxMessagesForChatSync int } @@ -40,7 +40,7 @@ func Load() Config { RedisAddr: redisAddr, RedisPassword: redisPassword, Port: port, - QueueName: "queue:history-sync", + HistorySyncQueueName: "queue:history-sync", MaxMessagesForChatSync: 10, } }