diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5969ab..f5d123f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Docker:** `evoapicloud/evolution-go:0.7.1` ### 🆕 New Features +- **Send Event endpoint** — new `POST /send/event` creates and sends a WhatsApp Event (group agenda). Body supports `name`, `description`, `startTime`/`endTime` (ISO 8601 with timezone or epoch seconds), optional `location`, `hasReminder`/`reminderOffsetSec` (e.g. `900`=15min, `3600`=1h, `86400`=1 day), `isScheduleCall`, `extraGuestsAllowed`, `isCanceled`, `mentionAll`/`mentionedJid`, and an optional `text` delivered as a leading message right before the event card (caption-style). The flag booleans are always sent explicitly (the WhatsApp client requires them present) and a 32-byte `MessageContextInfo` secret is set so the event's going/not-going responses decrypt. Note: `joinLink` only accepts WhatsApp call links (`call.whatsapp.com`); external URLs (site/YouTube) should go in `description`. Requires the matching event-stanza support in the bundled `whatsmeow` (`type=event` + ``). - **Test-send modal in Manager** — new modal in the embedded manager UI to test message sending directly from the panel, covering text, media and interactive message types. Useful for validating an instance right after pairing without leaving the manager. ### 🔧 Improvements / CI diff --git a/Evolution GO.postman_collection.json b/Evolution GO.postman_collection.json index 672f975d..6cf66922 100644 --- a/Evolution GO.postman_collection.json +++ b/Evolution GO.postman_collection.json @@ -725,6 +725,33 @@ }, "response": [] }, + { + "name": "Send Event", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"number\": \"120363000000000000@g.us\",\r\n \"name\": \"Sales meeting\",\r\n \"description\": \"Monthly alignment. External links (site/YouTube) go in this field.\",\r\n \"text\": \"📢 Reminder! Confirm your presence in the event below 👇\",\r\n \"startTime\": \"2026-06-25T20:00:00-03:00\",\r\n \"endTime\": \"2026-06-25T21:00:00-03:00\",\r\n \"location\": {\r\n \"name\": \"HQ\",\r\n \"latitude\": -16.6869,\r\n \"longitude\": -49.2648,\r\n \"address\": \"Av. Principal, 1000\"\r\n },\r\n \"hasReminder\": true,\r\n \"reminderOffsetSec\": 3600,\r\n \"mentionAll\": true\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/send/event", + "host": [ + "{{host}}" + ], + "path": [ + "send", + "event" + ] + } + }, + "response": [] + }, { "name": "Send Contact", "request": { diff --git a/docs/docs.go b/docs/docs.go index c089c1f9..7149f165 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2575,6 +2575,52 @@ const docTemplate = `{ } } }, + "/send/event": { + "post": { + "description": "Create and send a WhatsApp Event (group agenda). startTime/endTime accept an ISO 8601 (RFC3339) timestamp with timezone or epoch seconds. Typically sent to a group JID, e.g. 1203...@g.us.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp event message", + "parameters": [ + { + "description": "Event data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventStruct" + } + } + ], + "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/link": { "post": { "description": "Send a link message", @@ -4351,6 +4397,122 @@ const docTemplate = `{ } } }, + "github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventLocationStruct": { + "type": "object", + "properties": { + "address": { + "type": "string", + "example": "Av. Principal, 1000" + }, + "latitude": { + "type": "number", + "example": -16.6869 + }, + "longitude": { + "type": "number", + "example": -49.2648 + }, + "name": { + "type": "string", + "example": "Sede Grupo Mirandas" + } + } + }, + "github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventStruct": { + "type": "object", + "properties": { + "delay": { + "description": "Typing delay (milliseconds) before sending.", + "type": "integer" + }, + "description": { + "description": "Optional long description.", + "type": "string" + }, + "endTime": { + "description": "Optional event end. ISO 8601 with timezone or epoch seconds.", + "type": "string" + }, + "extraGuestsAllowed": { + "description": "Allow guests to invite extra guests.", + "type": "boolean" + }, + "formatJid": { + "description": "If false, skips JID formatting/validation of ` + "`" + `number` + "`" + `.", + "type": "boolean" + }, + "hasReminder": { + "description": "Enable a reminder for the event.", + "type": "boolean" + }, + "id": { + "description": "Optional custom message ID.", + "type": "string" + }, + "isCanceled": { + "description": "Cancel a previously created event.", + "type": "boolean" + }, + "isScheduleCall": { + "description": "Whether this event is a scheduled call.", + "type": "boolean" + }, + "joinLink": { + "description": "Optional call link (Meet, Zoom, etc.).", + "type": "string" + }, + "location": { + "description": "Optional location (name + coordinates + address).", + "allOf": [ + { + "$ref": "#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventLocationStruct" + } + ] + }, + "mentionAll": { + "description": "Mention every participant (groups only).", + "type": "boolean" + }, + "mentionedJid": { + "description": "JIDs to mention inside the event.", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "Event title (required).", + "type": "string", + "example": "Reuniao de vendas" + }, + "number": { + "description": "Destination JID (typically a group, e.g. 1203...@g.us).", + "type": "string", + "example": "120363000000000000@g.us" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.QuotedStruct" + } + ] + }, + "reminderOffsetSec": { + "description": "Seconds before startTime to fire the reminder (requires hasReminder).", + "type": "integer" + }, + "startTime": { + "description": "Event start (required). ISO 8601 with timezone or epoch seconds.", + "type": "string", + "example": "2026-06-25T20:00:00-03:00" + }, + "text": { + "description": "Optional text message sent right before the event card (acts like a\ncaption). The WhatsApp event has no caption field, so when set this is\ndelivered as a separate text message first, then the event card. Respects\nmentionAll/mentionedJid/delay.", + "type": "string" + } + } + }, "github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.LinkStruct": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 72a293c9..dd246951 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2567,6 +2567,52 @@ } } }, + "/send/event": { + "post": { + "description": "Create and send a WhatsApp Event (group agenda). startTime/endTime accept an ISO 8601 (RFC3339) timestamp with timezone or epoch seconds. Typically sent to a group JID, e.g. 1203...@g.us.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp event message", + "parameters": [ + { + "description": "Event data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventStruct" + } + } + ], + "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/link": { "post": { "description": "Send a link message", @@ -4343,6 +4389,122 @@ } } }, + "github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventLocationStruct": { + "type": "object", + "properties": { + "address": { + "type": "string", + "example": "Av. Principal, 1000" + }, + "latitude": { + "type": "number", + "example": -16.6869 + }, + "longitude": { + "type": "number", + "example": -49.2648 + }, + "name": { + "type": "string", + "example": "Sede Grupo Mirandas" + } + } + }, + "github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventStruct": { + "type": "object", + "properties": { + "delay": { + "description": "Typing delay (milliseconds) before sending.", + "type": "integer" + }, + "description": { + "description": "Optional long description.", + "type": "string" + }, + "endTime": { + "description": "Optional event end. ISO 8601 with timezone or epoch seconds.", + "type": "string" + }, + "extraGuestsAllowed": { + "description": "Allow guests to invite extra guests.", + "type": "boolean" + }, + "formatJid": { + "description": "If false, skips JID formatting/validation of `number`.", + "type": "boolean" + }, + "hasReminder": { + "description": "Enable a reminder for the event.", + "type": "boolean" + }, + "id": { + "description": "Optional custom message ID.", + "type": "string" + }, + "isCanceled": { + "description": "Cancel a previously created event.", + "type": "boolean" + }, + "isScheduleCall": { + "description": "Whether this event is a scheduled call.", + "type": "boolean" + }, + "joinLink": { + "description": "Optional call link (Meet, Zoom, etc.).", + "type": "string" + }, + "location": { + "description": "Optional location (name + coordinates + address).", + "allOf": [ + { + "$ref": "#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventLocationStruct" + } + ] + }, + "mentionAll": { + "description": "Mention every participant (groups only).", + "type": "boolean" + }, + "mentionedJid": { + "description": "JIDs to mention inside the event.", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "Event title (required).", + "type": "string", + "example": "Reuniao de vendas" + }, + "number": { + "description": "Destination JID (typically a group, e.g. 1203...@g.us).", + "type": "string", + "example": "120363000000000000@g.us" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.QuotedStruct" + } + ] + }, + "reminderOffsetSec": { + "description": "Seconds before startTime to fire the reminder (requires hasReminder).", + "type": "integer" + }, + "startTime": { + "description": "Event start (required). ISO 8601 with timezone or epoch seconds.", + "type": "string", + "example": "2026-06-25T20:00:00-03:00" + }, + "text": { + "description": "Optional text message sent right before the event card (acts like a\ncaption). The WhatsApp event has no caption field, so when set this is\ndelivered as a separate text message first, then the event card. Respects\nmentionAll/mentionedJid/delay.", + "type": "string" + } + } + }, "github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.LinkStruct": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f99331ae..518ea3d8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -592,6 +592,92 @@ definitions: vcard: $ref: '#/definitions/github_com_EvolutionAPI_evolution-go_pkg_utils.VCardStruct' type: object + github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventLocationStruct: + properties: + address: + example: Av. Principal, 1000 + type: string + latitude: + example: -16.6869 + type: number + longitude: + example: -49.2648 + type: number + name: + example: Sede Grupo Mirandas + type: string + type: object + github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventStruct: + properties: + delay: + description: Typing delay (milliseconds) before sending. + type: integer + description: + description: Optional long description. + type: string + endTime: + description: Optional event end. ISO 8601 with timezone or epoch seconds. + type: string + extraGuestsAllowed: + description: Allow guests to invite extra guests. + type: boolean + formatJid: + description: If false, skips JID formatting/validation of `number`. + type: boolean + hasReminder: + description: Enable a reminder for the event. + type: boolean + id: + description: Optional custom message ID. + type: string + isCanceled: + description: Cancel a previously created event. + type: boolean + isScheduleCall: + description: Whether this event is a scheduled call. + type: boolean + joinLink: + description: Optional call link (Meet, Zoom, etc.). + type: string + location: + allOf: + - $ref: '#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventLocationStruct' + description: Optional location (name + coordinates + address). + mentionAll: + description: Mention every participant (groups only). + type: boolean + mentionedJid: + description: JIDs to mention inside the event. + items: + type: string + type: array + name: + description: Event title (required). + example: Reuniao de vendas + type: string + number: + description: Destination JID (typically a group, e.g. 1203...@g.us). + example: 120363000000000000@g.us + type: string + quoted: + allOf: + - $ref: '#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.QuotedStruct' + description: Quoted (reply-to) context. + reminderOffsetSec: + description: Seconds before startTime to fire the reminder (requires hasReminder). + type: integer + startTime: + description: Event start (required). ISO 8601 with timezone or epoch seconds. + example: "2026-06-25T20:00:00-03:00" + type: string + text: + description: |- + Optional text message sent right before the event card (acts like a + caption). The WhatsApp event has no caption field, so when set this is + delivered as a separate text message first, then the event card. Respects + mentionAll/mentionedJid/delay. + type: string + type: object github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.LinkStruct: properties: delay: @@ -8311,6 +8397,38 @@ paths: summary: Send a contact message tags: - Send Message + /send/event: + post: + consumes: + - application/json + description: Create and send a WhatsApp Event (group agenda). startTime/endTime + accept an ISO 8601 (RFC3339) timestamp with timezone or epoch seconds. Typically + sent to a group JID, e.g. 1203...@g.us. + parameters: + - description: Event data + in: body + name: message + required: true + schema: + $ref: '#/definitions/github_com_EvolutionAPI_evolution-go_pkg_sendMessage_service.EventStruct' + produces: + - application/json + 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' + summary: Send a WhatsApp event message + tags: + - Send Message /send/link: post: consumes: diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index f51e7d96..91cfa0be 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -118,6 +118,7 @@ func (r *Routes) AssignRoutes(eng *gin.Engine) { routes.POST("/poll", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendPoll) routes.POST("/sticker", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendSticker) routes.POST("/location", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendLocation) + routes.POST("/event", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendEvent) 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) diff --git a/pkg/sendMessage/handler/send_handler.go b/pkg/sendMessage/handler/send_handler.go index 4d20e8b9..07e1de12 100644 --- a/pkg/sendMessage/handler/send_handler.go +++ b/pkg/sendMessage/handler/send_handler.go @@ -19,6 +19,7 @@ type SendHandler interface { SendPoll(ctx *gin.Context) SendSticker(ctx *gin.Context) SendLocation(ctx *gin.Context) + SendEvent(ctx *gin.Context) SendContact(ctx *gin.Context) SendButton(ctx *gin.Context) SendList(ctx *gin.Context) @@ -441,6 +442,57 @@ func (s *sendHandler) SendLocation(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) } +// Send an event message +// @Summary Send a WhatsApp event message +// @Description Create and send a WhatsApp Event (group agenda). startTime/endTime accept an ISO 8601 (RFC3339) timestamp with timezone or epoch seconds. Typically sent to a group JID, e.g. 1203...@g.us. +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.EventStruct true "Event 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/event [post] +func (s *sendHandler) SendEvent(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.EventStruct + 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 data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "event name is required"}) + return + } + + if data.StartTime.Unix() <= 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "startTime is required (ISO 8601 or epoch seconds)"}) + return + } + + message, err := s.sendMessageService.SendEvent(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}) +} + // Send a contact message // @Summary Send a contact message // @Description Send a contact message diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index c6ecdcdd..4690c27c 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -3,6 +3,7 @@ package send_service import ( "bytes" "context" + "crypto/rand" "encoding/base64" "encoding/json" "errors" @@ -41,6 +42,7 @@ type SendService interface { SendPoll(data *PollStruct, instance *instance_model.Instance) (*MessageSendStruct, error) SendSticker(data *StickerStruct, instance *instance_model.Instance) (*MessageSendStruct, error) SendLocation(data *LocationStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendEvent(data *EventStruct, instance *instance_model.Instance) (*MessageSendStruct, error) 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) @@ -151,6 +153,86 @@ type LocationStruct struct { Quoted QuotedStruct `json:"quoted"` } +// EventTime accepts epoch seconds (number or string) OR an ISO 8601 (RFC3339) +// timestamp with timezone, and normalizes everything to epoch seconds. +type EventTime int64 + +func (t *EventTime) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), `"`) + if s == "" || s == "null" { + return nil + } + if n, err := strconv.ParseInt(s, 10, 64); err == nil { + *t = EventTime(n) + return nil + } + if tm, err := time.Parse(time.RFC3339, s); err == nil { + *t = EventTime(tm.Unix()) + return nil + } + return fmt.Errorf("invalid time %q: use ISO 8601 (RFC3339) or epoch seconds", s) +} + +// Unix returns the time as epoch seconds. +func (t EventTime) Unix() int64 { return int64(t) } + +// EventLocationStruct is the optional location attached to an event. +type EventLocationStruct struct { + Name string `json:"name,omitempty" example:"Sede Grupo Mirandas"` + Latitude float64 `json:"latitude,omitempty" example:"-16.6869"` + Longitude float64 `json:"longitude,omitempty" example:"-49.2648"` + Address string `json:"address,omitempty" example:"Av. Principal, 1000"` +} + +// EventStruct is the body for POST /send/event. +// +// Sends a WhatsApp Event (group agenda). `startTime`/`endTime` accept either an +// ISO 8601 (RFC3339) string with timezone or epoch seconds (number or string). +// Only `number`, `name` and `startTime` are required. +type EventStruct struct { + // Destination JID (typically a group, e.g. 1203...@g.us). + Number string `json:"number" example:"120363000000000000@g.us"` + // Event title (required). + Name string `json:"name" example:"Reuniao de vendas"` + // Optional long description. + Description string `json:"description,omitempty"` + // Optional text message sent right before the event card (acts like a + // caption). The WhatsApp event has no caption field, so when set this is + // delivered as a separate text message first, then the event card. Respects + // mentionAll/mentionedJid/delay. + Text string `json:"text,omitempty"` + // Event start (required). ISO 8601 with timezone or epoch seconds. + StartTime EventTime `json:"startTime" swaggertype:"string" example:"2026-06-25T20:00:00-03:00"` + // Optional event end. ISO 8601 with timezone or epoch seconds. + EndTime EventTime `json:"endTime,omitempty" swaggertype:"string"` + // Optional location (name + coordinates + address). + Location *EventLocationStruct `json:"location,omitempty"` + // Optional call link (Meet, Zoom, etc.). + JoinLink string `json:"joinLink,omitempty"` + // Allow guests to invite extra guests. + ExtraGuestsAllowed bool `json:"extraGuestsAllowed,omitempty"` + // Whether this event is a scheduled call. + IsScheduleCall bool `json:"isScheduleCall,omitempty"` + // Enable a reminder for the event. + HasReminder bool `json:"hasReminder,omitempty"` + // Seconds before startTime to fire the reminder (requires hasReminder). + ReminderOffsetSec int64 `json:"reminderOffsetSec,omitempty"` + // Cancel a previously created event. + IsCanceled bool `json:"isCanceled,omitempty"` + // Optional custom message ID. + Id string `json:"id,omitempty"` + // Typing delay (milliseconds) before sending. + Delay int32 `json:"delay,omitempty"` + // JIDs to mention inside the event. + MentionedJID []string `json:"mentionedJid,omitempty"` + // Mention every participant (groups only). + MentionAll bool `json:"mentionAll,omitempty"` + // If false, skips JID formatting/validation of `number`. + FormatJid *bool `json:"formatJid,omitempty"` + // Quoted (reply-to) context. + Quoted QuotedStruct `json:"quoted,omitempty"` +} + type ContactStruct struct { Number string `json:"number"` Id string `json:"id"` @@ -1638,6 +1720,85 @@ func (s *sendService) SendLocation(data *LocationStruct, instance *instance_mode return message, nil } +func (s *sendService) SendEvent(data *EventStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + _, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + // Optional leading text message — the WhatsApp EventMessage has no caption + // field, so when `text` is set it is delivered as a separate text message + // right before the event card (same number, mentions and delay). + if data.Text != "" { + if _, err := s.SendText(&TextStruct{ + Number: data.Number, + Text: data.Text, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }, instance); err != nil { + return nil, fmt.Errorf("failed to send event text: %w", err) + } + } + + event := &waE2E.EventMessage{ + Name: proto.String(data.Name), + StartTime: proto.Int64(data.StartTime.Unix()), + } + if data.Description != "" { + event.Description = proto.String(data.Description) + } + if data.EndTime.Unix() > 0 { + event.EndTime = proto.Int64(data.EndTime.Unix()) + } + if data.JoinLink != "" { + event.JoinLink = proto.String(data.JoinLink) + } + // The official WhatsApp client always sends these booleans explicitly on + // creation; the server expects them present (a nil *bool is dropped from the + // wire and the event is silently discarded), so set them unconditionally. + event.IsCanceled = proto.Bool(data.IsCanceled) + event.IsScheduleCall = proto.Bool(data.IsScheduleCall) + event.ExtraGuestsAllowed = proto.Bool(data.ExtraGuestsAllowed) + if data.HasReminder { + event.HasReminder = proto.Bool(true) + if data.ReminderOffsetSec > 0 { + event.ReminderOffsetSec = proto.Int64(data.ReminderOffsetSec) + } + } + if data.Location != nil { + event.Location = &waE2E.LocationMessage{ + DegreesLatitude: proto.Float64(data.Location.Latitude), + DegreesLongitude: proto.Float64(data.Location.Longitude), + Name: proto.String(data.Location.Name), + Address: proto.String(data.Location.Address), + } + } + + // MessageSecret (32 bytes) is required so event responses (going/not_going) + // can be decrypted by the server — same pattern whatsmeow uses in BuildPollCreation. + secret := make([]byte, 32) + if _, err := rand.Read(secret); err != nil { + return nil, fmt.Errorf("failed to generate event message secret: %w", err) + } + + msg := &waE2E.Message{ + EventMessage: event, + MessageContextInfo: &waE2E.MessageContextInfo{MessageSecret: secret}, + } + + return s.SendMessage(instance, msg, "EventMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }) +} + func (s *sendService) SendContact(data *ContactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { _, err := s.ensureClientConnected(instance.Id) if err != nil { @@ -2127,6 +2288,12 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. Participant: proto.String(data.Quoted.Participant), QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, } + case "EventMessage": + msg.EventMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } case "ContactMessage": msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{ StanzaID: proto.String(data.Quoted.MessageID), @@ -2181,6 +2348,8 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} case "LocationMessage": msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} + case "EventMessage": + msg.EventMessage.ContextInfo = &waE2E.ContextInfo{} case "ContactMessage": msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} case "InteractiveMessage": @@ -2254,6 +2423,11 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} } msg.LocationMessage.ContextInfo.MentionedJID = mentionedJIDs + case "EventMessage": + if msg.EventMessage.ContextInfo == nil { + msg.EventMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.EventMessage.ContextInfo.MentionedJID = mentionedJIDs case "ContactMessage": if msg.ContactMessage.ContextInfo == nil { msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} @@ -2310,6 +2484,11 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} } msg.LocationMessage.ContextInfo.MentionedJID = data.MentionedJID + case "EventMessage": + if msg.EventMessage.ContextInfo == nil { + msg.EventMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.EventMessage.ContextInfo.MentionedJID = data.MentionedJID case "ContactMessage": if msg.ContactMessage.ContextInfo == nil { msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{}