From 2fdde7d0f0957807db0f433c053373f103a97ded Mon Sep 17 00:00:00 2001 From: Evolution API Dev Date: Sun, 5 Apr 2026 11:14:42 -0300 Subject: [PATCH 1/3] feat: add status@broadcast endpoints for WhatsApp status - Add POST /send/status/text endpoint for sending text status - Add POST /send/status/media endpoint for sending image/video status - Support both JSON (URL) and multipart/form-data (file upload) - Use Upload() with encryption for proper WhatsApp status format - Add HTTP status code validation for URL downloads - Add docker-compose.run.yml for local development --- docker-compose.run.yml | 62 +++++ pkg/routes/routes.go | 3 +- pkg/sendMessage/handler/send_handler.go | 118 ++++++++++ pkg/sendMessage/service/send_service.go | 290 +++++++++++++++++++++++- 4 files changed, 466 insertions(+), 7 deletions(-) create mode 100644 docker-compose.run.yml diff --git a/docker-compose.run.yml b/docker-compose.run.yml new file mode 100644 index 0000000..6ffe4e5 --- /dev/null +++ b/docker-compose.run.yml @@ -0,0 +1,62 @@ +version: '3.8' + +services: + evolution-go: + image: evolution-go:latest + restart: unless-stopped + ports: + - "4000:4000" + environment: + SERVER_PORT: 4000 + CLIENT_NAME: "evolution" + GLOBAL_API_KEY: "429683C4C977415CAAFCCE10F7D57E11" + POSTGRES_AUTH_DB: "postgresql://postgres:postgres@postgres:5432/evogo_auth?sslmode=disable" + POSTGRES_USERS_DB: "postgresql://postgres:postgres@postgres:5432/evogo_users?sslmode=disable" + DATABASE_SAVE_MESSAGES: "false" + WADEBUG: "DEBUG" + LOGTYPE: "console" + CONNECT_ON_STARTUP: "true" + WEBHOOKFILES: "true" + OS_NAME: "Linux" + WEBHOOK_URL: "" + AMQP_URL: "" + MINIO_ENABLED: "false" + EVENT_IGNORE_GROUP: "false" + EVENT_IGNORE_STATUS: "true" + QRCODE_MAX_COUNT: "5" + volumes: + - evolution_data:/app/dbdata + - evolution_logs:/app/logs + networks: + - evolution_network + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:15-alpine + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - evolution_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + evolution_data: + evolution_logs: + postgres_data: + +networks: + evolution_network: + driver: bridge diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 0064014..efeb340 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -121,7 +121,8 @@ func (r *Routes) AssignRoutes(eng *gin.Engine) { routes.POST("/contact", r.jidValidationMiddleware.ValidateContactFields(), r.sendHandler.SendContact) // TODO: send multiple contacts routes.POST("/button", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendButton) routes.POST("/list", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendList) - // TODO: send status + routes.POST("/status/text", r.sendHandler.SendStatusText) + routes.POST("/status/media", r.sendHandler.SendStatusMedia) } } routes = eng.Group("/user") diff --git a/pkg/sendMessage/handler/send_handler.go b/pkg/sendMessage/handler/send_handler.go index 7e4e43f..9394ccc 100644 --- a/pkg/sendMessage/handler/send_handler.go +++ b/pkg/sendMessage/handler/send_handler.go @@ -21,6 +21,8 @@ type SendHandler interface { SendContact(ctx *gin.Context) SendButton(ctx *gin.Context) SendList(ctx *gin.Context) + SendStatusText(ctx *gin.Context) + SendStatusMedia(ctx *gin.Context) } type sendHandler struct { @@ -570,6 +572,122 @@ func (s *sendHandler) SendList(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) } +func (s *sendHandler) SendStatusText(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.StatusTextStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Text == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "text is required"}) + return + } + + message, err := s.sendMessageService.SendStatusText(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +func (s *sendHandler) SendStatusMedia(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + contentType := ctx.ContentType() + + var data *send_service.StatusMediaStruct + + if strings.HasPrefix(contentType, "multipart/form-data") { + mediaType := ctx.PostForm("type") + if mediaType == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "media type is required"}) + return + } + + if mediaType != "image" && mediaType != "video" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "type must be 'image' or 'video'"}) + return + } + + caption := ctx.PostForm("caption") + id := ctx.PostForm("id") + + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) + return + } + + fileData, err := file.Open() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot open file"}) + return + } + defer fileData.Close() + fileBytes, err := io.ReadAll(fileData) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot read file"}) + return + } + + data = &send_service.StatusMediaStruct{ + Type: mediaType, + Caption: caption, + Id: id, + } + + message, err := s.sendMessageService.SendStatusMediaFile(data, fileBytes, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) + } else { + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Url == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "url is required"}) + return + } + + if data.Type != "image" && data.Type != "video" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "type must be 'image' or 'video'"}) + return + } + + message, err := s.sendMessageService.SendStatusMediaUrl(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) + } +} + func NewSendHandler( sendMessageService send_service.SendService, ) SendHandler { diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index 09db21a..569cb93 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -43,6 +43,9 @@ type SendService interface { SendContact(data *ContactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) SendButton(data *ButtonStruct, instance *instance_model.Instance) (*MessageSendStruct, error) SendList(data *ListStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendStatusText(data *StatusTextStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendStatusMediaUrl(data *StatusMediaStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendStatusMediaFile(data *StatusMediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) } type sendService struct { @@ -234,15 +237,29 @@ type CarouselCardStruct struct { } type CarouselStruct struct { - Number string `json:"number"` - Body string `json:"body,omitempty"` - Footer string `json:"footer,omitempty"` - Delay int32 `json:"delay"` - FormatJid *bool `json:"formatJid,omitempty"` - Quoted QuotedStruct `json:"quoted"` + Number string `json:"number"` + Body string `json:"body,omitempty"` + Footer string `json:"footer,omitempty"` + Delay int32 `json:"delay"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` Cards []CarouselCardStruct `json:"cards"` } +type StatusTextStruct struct { + Text string `json:"text"` + Id string `json:"id"` + Font int32 `json:"font,omitempty"` + BackgroundColor string `json:"backgroundColor,omitempty"` +} + +type StatusMediaStruct struct { + Type string `json:"type"` + Url string `json:"url"` + Caption string `json:"caption"` + Id string `json:"id"` +} + type MessageSendStruct struct { Info types.MessageInfo Message *waE2E.Message @@ -2541,6 +2558,267 @@ func (s *sendService) SendCarousel(data *CarouselStruct, instance *instance_mode return message, nil } +func (s *sendService) SendStatusText(data *StatusTextStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + if data.Text == "" { + return nil, errors.New("text is required") + } + + msg := &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: &data.Text, + }, + } + + messageID := data.Id + if messageID == "" { + messageID = client.GenerateMessageID() + } + + recipient := types.NewJID("status", "broadcast") + + response, err := client.SendMessage(context.Background(), recipient, msg, whatsmeow.SendRequestExtra{ID: messageID}) + if err != nil { + return nil, err + } + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: recipient, + Sender: *client.Store.ID, + IsFromMe: true, + IsGroup: false, + }, + ID: messageID, + Timestamp: time.Now(), + ServerID: response.ServerID, + Type: "StatusTextMessage", + } + + messageSent := &MessageSendStruct{ + Info: messageInfo, + Message: msg, + MessageContextInfo: &waE2E.ContextInfo{ + StanzaID: proto.String(""), + Participant: proto.String(""), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + }, + } + + postMap := make(map[string]interface{}) + postMap["event"] = "SendStatus" + messageData := make(map[string]interface{}) + messageData["Info"] = messageSent.Info + msgBytes, err := json.Marshal(messageSent.Message) + if err != nil { + return nil, fmt.Errorf("failed to marshal message: %v", err) + } + var msgMap map[string]interface{} + if err := json.Unmarshal(msgBytes, &msgMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal message: %v", err) + } + messageData["Message"] = msgMap + messageData["MessageContextInfo"] = messageSent.MessageContextInfo + postMap["data"] = messageData + postMap["instanceToken"] = instance.Token + postMap["instanceId"] = instance.Id + postMap["instanceName"] = instance.Name + + values, err := json.Marshal(postMap) + if err != nil { + return nil, err + } + go s.whatsmeowService.CallWebhook(instance, "sendstatus", values) + if s.config.AmqpGlobalEnabled || s.config.NatsGlobalEnabled { + go s.whatsmeowService.SendToGlobalQueues("SendStatus", values, instance.Id) + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status text sent successfully", instance.Id) + return messageSent, nil +} + +func (s *sendService) SendStatusMediaUrl(data *StatusMediaStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + if data.Url == "" { + return nil, errors.New("url is required") + } + if data.Type != "image" && data.Type != "video" { + return nil, errors.New("type must be 'image' or 'video'") + } + + req, err := http.NewRequest("GET", data.Url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", "Evolution-GO/1.0") + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to download file from URL: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, fmt.Errorf("failed to download file: HTTP status %d", resp.StatusCode) + } + + fileData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return s.sendStatusMedia(client, data, fileData, instance) +} + +func (s *sendService) SendStatusMediaFile(data *StatusMediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + if data.Type != "image" && data.Type != "video" { + return nil, errors.New("type must be 'image' or 'video'") + } + + return s.sendStatusMedia(client, data, fileData, instance) +} + +func (s *sendService) sendStatusMedia(client *whatsmeow.Client, data *StatusMediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) { + mime, _ := mimetype.DetectReader(bytes.NewReader(fileData)) + mimeType := mime.String() + + var uploadType whatsmeow.MediaType + switch data.Type { + case "image": + if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { + return nil, fmt.Errorf("invalid file format: '%s'. Only 'image/jpeg', 'image/png' and 'image/webp' are accepted", mimeType) + } + if mimeType == "image/webp" { + mimeType = "image/jpeg" + } + uploadType = whatsmeow.MediaImage + case "video": + if mimeType != "video/mp4" { + return nil, fmt.Errorf("invalid file format: '%s'. Only 'video/mp4' is accepted", mimeType) + } + uploadType = whatsmeow.MediaVideo + default: + return nil, errors.New("invalid media type") + } + + uploaded, err := client.Upload(context.Background(), fileData, uploadType) + if err != nil { + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status media uploaded, size: %d", instance.Id, uploaded.FileLength) + + var media *waE2E.Message + var mediaType string + + switch data.Type { + case "image": + media = &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + mediaType = "ImageMessage" + case "video": + media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + mediaType = "VideoMessage" + } + + messageID := data.Id + if messageID == "" { + messageID = client.GenerateMessageID() + } + + recipient := types.NewJID("status", "broadcast") + + response, err := client.SendMessage(context.Background(), recipient, media, whatsmeow.SendRequestExtra{ID: messageID}) + if err != nil { + return nil, err + } + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: recipient, + Sender: *client.Store.ID, + IsFromMe: true, + IsGroup: false, + }, + ID: messageID, + Timestamp: time.Now(), + ServerID: response.ServerID, + Type: mediaType, + } + + messageSent := &MessageSendStruct{ + Info: messageInfo, + Message: media, + MessageContextInfo: &waE2E.ContextInfo{ + StanzaID: proto.String(""), + Participant: proto.String(""), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + }, + } + + postMap := make(map[string]interface{}) + postMap["event"] = "SendStatus" + messageData := make(map[string]interface{}) + messageData["Info"] = messageSent.Info + msgBytes, err := json.Marshal(messageSent.Message) + if err != nil { + return nil, fmt.Errorf("failed to marshal message: %v", err) + } + var msgMap map[string]interface{} + if err := json.Unmarshal(msgBytes, &msgMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal message: %v", err) + } + messageData["Message"] = msgMap + messageData["MessageContextInfo"] = messageSent.MessageContextInfo + postMap["data"] = messageData + postMap["instanceToken"] = instance.Token + postMap["instanceId"] = instance.Id + postMap["instanceName"] = instance.Name + + values, err := json.Marshal(postMap) + if err != nil { + return nil, err + } + go s.whatsmeowService.CallWebhook(instance, "sendstatus", values) + if s.config.AmqpGlobalEnabled || s.config.NatsGlobalEnabled { + go s.whatsmeowService.SendToGlobalQueues("SendStatus", values, instance.Id) + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status media sent successfully", instance.Id) + return messageSent, nil +} + func NewSendService( clientPointer map[string]*whatsmeow.Client, whatsmeowService whatsmeow_service.WhatsmeowService, From 1761dc8a3520f41028948fb8f96add4de640cfdb Mon Sep 17 00:00:00 2001 From: Evolution API Dev Date: Sun, 5 Apr 2026 11:38:32 -0300 Subject: [PATCH 2/3] docs: add Swagger documentation for status@broadcast endpoints - Add OpenAPI docs for POST /send/status/text - Add OpenAPI docs for POST /send/status/media (supports JSON and multipart) - Add StatusTextStruct and StatusMediaStruct definitions - Add Swagger annotations to handler functions --- docs/docs.go | 205 ++++++++++++++++++++++++ pkg/sendMessage/handler/send_handler.go | 26 +++ 2 files changed, 231 insertions(+) diff --git a/docs/docs.go b/docs/docs.go index 0843d82..5de603c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2175,6 +2175,167 @@ const docTemplate = `{ } } }, + "/send/list": { + "post": { + "description": "Send a list message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a list message", + "parameters": [ + { + "description": "List message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_Zapbox-API_evolution-go_pkg_sendMessage_service.ListStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/status/text": { + "post": { + "description": "Send a WhatsApp text status to status@broadcast", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp text status", + "parameters": [ + { + "description": "Status text data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_Zapbox-API_evolution-go_pkg_sendMessage_service.StatusTextStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/status/media": { + "post": { + "description": "Send an image or video status to status@broadcast. Supports JSON (URL) or multipart/form-data (file upload)", + "consumes": [ + "application/json", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp media status (image/video)", + "parameters": [ + { + "description": "Media type: image or video", + "name": "type", + "in": "formData", + "required": true, + "type": "string" + }, + { + "description": "Media file (for multipart upload)", + "name": "file", + "in": "formData", + "type": "file" + }, + { + "description": "Media URL (for JSON upload)", + "name": "url", + "in": "formData", + "type": "string" + }, + { + "description": "Caption for the media", + "name": "caption", + "in": "formData", + "type": "string" + }, + { + "description": "Custom message ID", + "name": "id", + "in": "formData", + "type": "string" + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, "/unlabel/message": { "post": { "description": "Remove label from message", @@ -3198,6 +3359,50 @@ const docTemplate = `{ } } }, + "github_com_Zapbox-API_evolution-go_pkg_sendMessage_service.StatusTextStruct": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text content of the status" + }, + "id": { + "type": "string", + "description": "Custom message ID (optional)" + }, + "font": { + "type": "integer", + "description": "Font style (0-4)" + }, + "backgroundColor": { + "type": "string", + "description": "Background color in hex format (e.g., #FF0000)" + } + }, + "required": ["text"] + }, + "github_com_Zapbox-API_evolution-go_pkg_sendMessage_service.StatusMediaStruct": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Media type: image or video" + }, + "url": { + "type": "string", + "description": "URL of the media (for JSON upload)" + }, + "caption": { + "type": "string", + "description": "Caption for the media" + }, + "id": { + "type": "string", + "description": "Custom message ID (optional)" + } + }, + "required": ["type"] + }, "github_com_Zapbox-API_evolution-go_pkg_user_service.CheckUserStruct": { "type": "object", "properties": { diff --git a/pkg/sendMessage/handler/send_handler.go b/pkg/sendMessage/handler/send_handler.go index 9394ccc..742f163 100644 --- a/pkg/sendMessage/handler/send_handler.go +++ b/pkg/sendMessage/handler/send_handler.go @@ -572,6 +572,17 @@ func (s *sendHandler) SendList(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) } +// Send a text status message +// @Summary Send a WhatsApp text status +// @Description Send a text status message to status@broadcast +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.StatusTextStruct true "Status text data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/status/text [post] func (s *sendHandler) SendStatusText(ctx *gin.Context) { getInstance := ctx.MustGet("instance") @@ -602,6 +613,21 @@ func (s *sendHandler) SendStatusText(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) } +// Send a media status message (image or video) +// @Summary Send a WhatsApp media status (image/video) +// @Description Send an image or video status to status@broadcast. Supports JSON (URL) or multipart/form-data (file upload) +// @Tags Send Message +// @Accept json, multipart/form-data +// @Produce json +// @Param type formData string true "Media type: image or video" +// @Param file formData file false "Media file (for multipart upload)" +// @Param url formData string false "Media URL (for JSON upload)" +// @Param caption formData string false "Caption for the media" +// @Param id formData string false "Custom message ID" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/status/media [post] func (s *sendHandler) SendStatusMedia(ctx *gin.Context) { getInstance := ctx.MustGet("instance") From 0fa9c9df25882cf5fcf47fab6fee21f3d587edfd Mon Sep 17 00:00:00 2001 From: Evolution API Dev Date: Sun, 5 Apr 2026 12:38:05 -0300 Subject: [PATCH 3/3] fix: address PR review comments - Fix binding: use new() instead of var with pointer-to-pointer - Remove unused font and backgroundColor fields from StatusTextStruct - Extract webhook/queue logic into shared sendStatusWebhook helper - Update Swagger docs to reflect StatusTextStruct changes --- docs/docs.go | 8 ---- pkg/sendMessage/handler/send_handler.go | 8 ++-- pkg/sendMessage/service/send_service.go | 53 ++++++++++++++----------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 5de603c..8ccea12 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3369,14 +3369,6 @@ const docTemplate = `{ "id": { "type": "string", "description": "Custom message ID (optional)" - }, - "font": { - "type": "integer", - "description": "Font style (0-4)" - }, - "backgroundColor": { - "type": "string", - "description": "Background color in hex format (e.g., #FF0000)" } }, "required": ["text"] diff --git a/pkg/sendMessage/handler/send_handler.go b/pkg/sendMessage/handler/send_handler.go index 742f163..dacc2b8 100644 --- a/pkg/sendMessage/handler/send_handler.go +++ b/pkg/sendMessage/handler/send_handler.go @@ -592,8 +592,8 @@ func (s *sendHandler) SendStatusText(ctx *gin.Context) { return } - var data *send_service.StatusTextStruct - err := ctx.ShouldBindBodyWithJSON(&data) + data := new(send_service.StatusTextStruct) + err := ctx.ShouldBindBodyWithJSON(data) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -639,7 +639,7 @@ func (s *sendHandler) SendStatusMedia(ctx *gin.Context) { contentType := ctx.ContentType() - var data *send_service.StatusMediaStruct + data := new(send_service.StatusMediaStruct) if strings.HasPrefix(contentType, "multipart/form-data") { mediaType := ctx.PostForm("type") @@ -688,7 +688,7 @@ func (s *sendHandler) SendStatusMedia(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) } else { - err := ctx.ShouldBindBodyWithJSON(&data) + err := ctx.ShouldBindBodyWithJSON(data) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index 569cb93..fe80dd5 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -247,10 +247,8 @@ type CarouselStruct struct { } type StatusTextStruct struct { - Text string `json:"text"` - Id string `json:"id"` - Font int32 `json:"font,omitempty"` - BackgroundColor string `json:"backgroundColor,omitempty"` + Text string `json:"text"` + Id string `json:"id"` } type StatusMediaStruct struct { @@ -2638,6 +2636,7 @@ func (s *sendService) SendStatusText(data *StatusTextStruct, instance *instance_ } s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status text sent successfully", instance.Id) + s.sendStatusWebhook(messageSent, instance, "text") return messageSent, nil } @@ -2787,17 +2786,38 @@ func (s *sendService) sendStatusMedia(client *whatsmeow.Client, data *StatusMedi }, } + s.sendStatusWebhook(messageSent, instance, "media") + return messageSent, nil +} + +func NewSendService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + config *config.Config, + loggerWrapper *logger_wrapper.LoggerManager, +) SendService { + return &sendService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + config: config, + loggerWrapper: loggerWrapper, + } +} + +func (s *sendService) sendStatusWebhook(messageSent *MessageSendStruct, instance *instance_model.Instance, messageType string) { postMap := make(map[string]interface{}) postMap["event"] = "SendStatus" messageData := make(map[string]interface{}) messageData["Info"] = messageSent.Info msgBytes, err := json.Marshal(messageSent.Message) if err != nil { - return nil, fmt.Errorf("failed to marshal message: %v", err) + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to marshal status message: %v", instance.Id, err) + return } var msgMap map[string]interface{} if err := json.Unmarshal(msgBytes, &msgMap); err != nil { - return nil, fmt.Errorf("failed to unmarshal message: %v", err) + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to unmarshal status message: %v", instance.Id, err) + return } messageData["Message"] = msgMap messageData["MessageContextInfo"] = messageSent.MessageContextInfo @@ -2808,27 +2828,12 @@ func (s *sendService) sendStatusMedia(client *whatsmeow.Client, data *StatusMedi values, err := json.Marshal(postMap) if err != nil { - return nil, err + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to marshal webhook payload: %v", instance.Id, err) + return } go s.whatsmeowService.CallWebhook(instance, "sendstatus", values) if s.config.AmqpGlobalEnabled || s.config.NatsGlobalEnabled { go s.whatsmeowService.SendToGlobalQueues("SendStatus", values, instance.Id) } - - s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status media sent successfully", instance.Id) - return messageSent, nil -} - -func NewSendService( - clientPointer map[string]*whatsmeow.Client, - whatsmeowService whatsmeow_service.WhatsmeowService, - config *config.Config, - loggerWrapper *logger_wrapper.LoggerManager, -) SendService { - return &sendService{ - clientPointer: clientPointer, - whatsmeowService: whatsmeowService, - config: config, - loggerWrapper: loggerWrapper, - } + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status %s sent successfully", instance.Id, messageType) }