From 46801c62f6bf709983bc6fc7bab4cde8812f4902 Mon Sep 17 00:00:00 2001 From: Bruno Henrique Silva Contente Date: Sun, 26 Apr 2026 23:24:00 -0300 Subject: [PATCH] feat(message): add /message/markplayed endpoint for played receipt Adds a new POST /message/markplayed endpoint that sends a 'played' receipt (whatsmeow types.ReceiptTypePlayed), making the microphone icon turn blue on the sender's WhatsApp for audio messages. Mirrors the existing /message/markread structure (handler -> service -> route + JID validation middleware). 'read' and 'played' are distinct receipts in the WhatsApp protocol and can be sent independently. --- pkg/message/handler/message_handler.go | 51 ++++++++++++++++++++++++++ pkg/message/service/message_service.go | 29 +++++++++++++++ pkg/routes/routes.go | 1 + 3 files changed, 81 insertions(+) diff --git a/pkg/message/handler/message_handler.go b/pkg/message/handler/message_handler.go index c283ccf..a64c7ca 100644 --- a/pkg/message/handler/message_handler.go +++ b/pkg/message/handler/message_handler.go @@ -12,6 +12,7 @@ type MessageHandler interface { React(ctx *gin.Context) ChatPresence(ctx *gin.Context) MarkRead(ctx *gin.Context) + MarkPlayed(ctx *gin.Context) DownloadMedia(ctx *gin.Context) GetMessageStatus(ctx *gin.Context) DeleteMessageEveryone(ctx *gin.Context) @@ -168,6 +169,56 @@ func (m *messageHandler) MarkRead(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) } +// MarkPlayed mark an audio message as played (blue mic icon) +// @Summary Mark an audio message as played +// @Description Mark an audio message as played +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.MarkPlayedStruct true "Mark an audio message as played" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/markplayed [post] +func (m *messageHandler) MarkPlayed(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 *message_service.MarkPlayedStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if len(data.Id) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + return + } + + ts, err := m.messageService.MarkPlayed(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + // DownloadImage download an image // @Summary Download an image // @Description Download an image diff --git a/pkg/message/service/message_service.go b/pkg/message/service/message_service.go index b2b21c7..932b9ce 100644 --- a/pkg/message/service/message_service.go +++ b/pkg/message/service/message_service.go @@ -27,6 +27,7 @@ type MessageService interface { React(data *ReactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) ChatPresence(data *ChatPresenceStruct, instance *instance_model.Instance) (string, error) MarkRead(data *MarkReadStruct, instance *instance_model.Instance) (string, error) + MarkPlayed(data *MarkPlayedStruct, instance *instance_model.Instance) (string, error) DownloadMedia(data *DownloadMediaStruct, instance *instance_model.Instance, request *http.Request) (*dataurl.DataURL, string, error) GetMessageStatus(data *MessageStatusStruct, instance *instance_model.Instance) (*message_model.Message, string, error) DeleteMessageEveryone(data *MessageStruct, instance *instance_model.Instance) (string, string, error) @@ -59,6 +60,11 @@ type MarkReadStruct struct { Number string `json:"number"` } +type MarkPlayedStruct struct { + Id []string `json:"id"` + Number string `json:"number"` +} + type DownloadMediaStruct struct { Message *waE2E.Message `json:"message"` } @@ -258,6 +264,29 @@ func (m *messageService) MarkRead(data *MarkReadStruct, instance *instance_model return ts.String(), nil } +func (m *messageService) MarkPlayed(data *MarkPlayedStruct, instance *instance_model.Instance) (string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + jid, ok := utils.ParseJID(data.Number) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.MarkRead(context.Background(), data.Id, time.Now(), jid, jid, types.ReceiptTypePlayed) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error marking message as played: %v", instance.Id, err) + return "", errors.New("error marking message as played") + } + + return ts.String(), nil +} + func (m *messageService) DownloadMedia(data *DownloadMediaStruct, instance *instance_model.Instance, request *http.Request) (*dataurl.DataURL, string, error) { client, err := m.ensureClientConnected(instance.Id) if err != nil { diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index f51e7d9..3c6c474 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -151,6 +151,7 @@ func (r *Routes) AssignRoutes(eng *gin.Engine) { routes.POST("/react", r.jidValidationMiddleware.ValidateJIDFields("number"), r.messageHandler.React) routes.POST("/presence", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.ChatPresence) routes.POST("/markread", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.MarkRead) + routes.POST("/markplayed", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.MarkPlayed) routes.POST("/downloadmedia", r.messageHandler.DownloadMedia) routes.POST("/status", r.messageHandler.GetMessageStatus) routes.POST("/delete", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.DeleteMessageEveryone)