Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/wiki/recursos-avancados/events-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,31 @@ O Evolution GO usa dois níveis de classificação de eventos:
- `Receipt` - Confirmação de leitura (`READ_RECEIPT`)
- Reações, edições, deleções de mensagens

#### Edição de mensagem recebida

Quando um contato edita uma mensagem, o evento continua sendo `Message` (subscribe `MESSAGE`). Não existe categoria separada `MESSAGE_EDIT`.

Após descriptografia, o payload inclui:

- `IsEdit: true`
- `messageType: "edit"`
- `Message.protocolMessage.typeName: "MESSAGE_EDIT"` (o campo numérico `type` continua presente)
- Texto novo em `Message.protocolMessage.editedMessage` (ex.: `conversation` ou `extendedTextMessage`)
- ID da mensagem original em `Message.protocolMessage.key.ID` (ou, antes do decrypt, em `secretEncryptedMessage.targetMessageKey.ID`)

**Limitação**: o decrypt usa o `messageSecret` da mensagem original armazenado na sessão e deve rodar **antes** de qualquer normalização LID→PN do `Info.Sender`/`Chat`. Se o decrypt falhar, o webhook ainda chega com `IsEdit: true`, `messageType: "edit"` e `decryptFailed: true` (pode manter `secretEncryptedMessage` sem texto em claro).

#### Exclusão (revoke) de mensagem

Quando um contato apaga uma mensagem “para todos”, o evento também é `Message` (subscribe `MESSAGE`).

O payload inclui:

- `IsRevoke: true`
- `messageType: "revoke"`
- `Message.protocolMessage.typeName: "REVOKE"` (equivalente ao `type: 0` / `Info.Edit: "7"`)
- ID da mensagem apagada em `Message.protocolMessage.key.ID`

### Eventos de Grupos

**Categoria**: `GROUP`
Expand Down
83 changes: 83 additions & 0 deletions pkg/whatsmeow/service/whatsmeow.go
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,46 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
return
}

// Decrypt MESSAGE_EDIT BEFORE LID/PN JID swap. whatsmeow derives the edit
// key from Info.Sender/Chat as received on the wire; swapping first causes
// cipher: message authentication failed even when the secret exists.
secretEditEnvelope := false
decryptFailed := false
if enc := evt.Message.GetSecretEncryptedMessage(); enc != nil &&
enc.GetSecretEncType() == waE2E.SecretEncryptedMessage_MESSAGE_EDIT {
secretEditEnvelope = true
evt.IsEdit = true

client := mycli.clientPointer[mycli.userID]
if client == nil {
client = mycli.WAClient
}
if client == nil {
decryptFailed = true
mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn(
"[%s] No client available to decrypt secret encrypted message edit", mycli.userID)
} else {
decrypted, err := client.DecryptSecretEncryptedMessage(context.Background(), evt)
if err != nil {
decryptFailed = true
mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn(
"[%s] Failed to decrypt secret encrypted message edit: %v",
mycli.userID, err)
} else {
evt.Message = decrypted
mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo(
"[%s] Decrypted secret encrypted message edit for %s", mycli.userID, evt.Info.ID)
}
}
}

// EditedMessage wrapper (legacy wire format or post-decrypt). Do not call
// UnwrapRaw here — it resets Message from RawMessage and would undo decrypt.
if evt.Message != nil && evt.Message.GetEditedMessage().GetMessage() != nil {
evt.Message = evt.Message.GetEditedMessage().GetMessage()
evt.IsEdit = true
}

// Trata o caso especial onde Sender é @lid e SenderAlt é @s.whatsapp.net
// Neste caso, devemos inverter: Sender e Chat devem ser @s.whatsapp.net, SenderAlt deve ser @lid
senderStr := evt.Info.Sender.String()
Expand Down Expand Up @@ -1242,6 +1282,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
return
}

if parsedMessageType == "edit" || secretEditEnvelope {
evt.IsEdit = true
}

if postMap["data"] != nil {
jsonBytes, err := json.Marshal(postMap["data"])
if err != nil {
Expand All @@ -1266,6 +1310,29 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
dataMap = make(map[string]interface{})
}

// Explicit action flags for edit/revoke — protocolMessage.type alone is a
// numeric enum (0 = REVOKE, 14 = MESSAGE_EDIT) and Info.Edit is opaque.
switch parsedMessageType {
case "edit":
dataMap["IsEdit"] = true
dataMap["messageType"] = "edit"
setProtocolMessageTypeName(dataMap, "MESSAGE_EDIT")
case "revoke":
dataMap["IsRevoke"] = true
dataMap["messageType"] = "revoke"
setProtocolMessageTypeName(dataMap, "REVOKE")
default:
// Decrypt failed or plaintext not yet classified as "edit", but envelope
// already identified the event as an incoming message edit.
if secretEditEnvelope {
dataMap["IsEdit"] = true
dataMap["messageType"] = "edit"
}
}
if decryptFailed {
dataMap["decryptFailed"] = true
}

referral := extractReferralFromMessage(evt.Message)

if evt.Message.GetPollUpdateMessage() != nil {
Expand Down Expand Up @@ -2881,6 +2948,22 @@ func (w *whatsmeowService) ConfirmPasskey(instanceId string) error {
return nil
}

// setProtocolMessageTypeName adds a human-readable protocolMessage.typeName
// (e.g. REVOKE, MESSAGE_EDIT) without replacing the numeric type enum.
func setProtocolMessageTypeName(dataMap map[string]interface{}, typeName string) {
message, ok := dataMap["Message"].(map[string]interface{})
if !ok {
return
}
pm, ok := message["protocolMessage"].(map[string]interface{})
if !ok {
return
}
pm["typeName"] = typeName
message["protocolMessage"] = pm
dataMap["Message"] = message
}

// cleanSenderID remove a parte ":numero" do sender ID para exibir apenas o remoteJid correto
// Exemplo: "557499879409:3@s.whatsapp.net" -> "557499879409@s.whatsapp.net"
func cleanSenderID(senderID string) string {
Expand Down