1
0
Fork 0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-05-15 10:16:52 -04:00
mautrix-whatsapp/pkg/connector/handlewhatsapp.go

913 lines
34 KiB
Go
Raw Permalink Normal View History

// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2024-08-13 14:11:10 +03:00
package connector
import (
"context"
"fmt"
"strconv"
"strings"
"time"
2024-08-13 14:11:10 +03:00
"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow"
2024-09-10 15:56:51 +03:00
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/store"
2024-08-13 14:11:10 +03:00
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
2024-08-13 14:11:10 +03:00
"go.mau.fi/mautrix-whatsapp/pkg/waid"
2024-08-13 14:11:10 +03:00
)
const (
2024-09-10 15:56:51 +03:00
WANotLoggedIn status.BridgeStateErrorCode = "wa-not-logged-in"
2024-09-06 17:41:26 +03:00
WALoggedOut status.BridgeStateErrorCode = "wa-logged-out"
WAMainDeviceGone status.BridgeStateErrorCode = "wa-main-device-gone"
WAUnknownLogout status.BridgeStateErrorCode = "wa-unknown-logout"
WANotConnected status.BridgeStateErrorCode = "wa-not-connected"
WAConnecting status.BridgeStateErrorCode = "wa-connecting"
WAKeepaliveTimeout status.BridgeStateErrorCode = "wa-keepalive-timeout"
WAPhoneOffline status.BridgeStateErrorCode = "wa-phone-offline"
WAConnectionFailed status.BridgeStateErrorCode = "wa-connection-failed"
WADisconnected status.BridgeStateErrorCode = "wa-transient-disconnect"
WAStreamReplaced status.BridgeStateErrorCode = "wa-stream-replaced"
WAStreamError status.BridgeStateErrorCode = "wa-stream-error"
WAClientOutdated status.BridgeStateErrorCode = "wa-client-outdated"
WATemporaryBan status.BridgeStateErrorCode = "wa-temporary-ban"
2024-08-13 14:11:10 +03:00
)
2024-09-06 17:41:26 +03:00
func init() {
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
WALoggedOut: "You were logged out from another device. Relogin to continue using the bridge.",
WANotLoggedIn: "You're not logged into WhatsApp. Relogin to continue using the bridge.",
2024-09-06 17:41:26 +03:00
WAMainDeviceGone: "Your phone was logged out from WhatsApp. Relogin to continue using the bridge.",
WAUnknownLogout: "You were logged out for an unknown reason. Relogin to continue using the bridge.",
WANotConnected: "You're not connected to WhatsApp",
WAConnecting: "Reconnecting to WhatsApp...",
WAKeepaliveTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
WAPhoneOffline: "Your phone hasn't been seen in over 12 days. The bridge is currently connected, but will get disconnected if you don't open the app soon.",
WAConnectionFailed: "Connecting to the WhatsApp web servers failed.",
WADisconnected: "Disconnected from WhatsApp. Trying to reconnect.",
WAClientOutdated: "Connect failure: 405 client outdated. Bridge must be updated.",
WAStreamReplaced: "Stream replaced: the bridge was started in another location.",
})
}
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
2024-08-13 14:11:10 +03:00
log := wa.UserLogin.Log
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
2024-08-13 14:11:10 +03:00
success = true
2024-08-13 14:11:10 +03:00
switch evt := rawEvt.(type) {
2024-08-15 18:14:49 +03:00
case *events.Message:
success = wa.handleWAMessage(ctx, evt)
2024-08-13 14:11:10 +03:00
case *events.Receipt:
success = wa.handleWAReceipt(ctx, evt)
case *events.ChatPresence:
wa.handleWAChatPresence(ctx, evt)
case *events.UndecryptableMessage:
success = wa.handleWAUndecryptableMessage(ctx, evt)
case *events.CallOffer:
success = wa.handleWACallStart(ctx, evt.GroupJID, evt.CallCreator, evt.CallCreatorAlt, evt.CallID, "", evt.Timestamp)
case *events.CallOfferNotice:
success = wa.handleWACallStart(ctx, evt.GroupJID, evt.CallCreator, evt.CallCreatorAlt, evt.CallID, evt.Type, evt.Timestamp)
case *events.CallTerminate, *events.CallRelayLatency, *events.CallAccept, *events.UnknownCallEvent:
// ignore
case *events.IdentityChange:
wa.handleWAIdentityChange(ctx, evt)
case *events.MarkChatAsRead:
success = wa.handleWAMarkChatAsRead(ctx, evt)
case *events.DeleteForMe:
success = wa.handleWADeleteForMe(ctx, evt)
case *events.DeleteChat:
success = wa.handleWADeleteChat(ctx, evt)
case *events.Mute:
success = wa.handleWAMute(evt)
case *events.Archive:
success = wa.handleWAArchive(evt)
case *events.Pin:
success = wa.handleWAPin(evt)
case *events.HistorySync:
wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received")
case *events.MediaRetry:
wa.phoneSeen(evt.Timestamp)
success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success
case *events.GroupInfo:
success = wa.handleWAGroupInfoChange(ctx, evt)
case *events.JoinedGroup:
success = wa.handleWAJoinedGroup(ctx, evt)
case *events.NewsletterJoin:
success = wa.handleWANewsletterJoin(ctx, evt)
case *events.NewsletterLeave:
success = wa.handleWANewsletterLeave(evt)
case *events.Picture:
success = wa.handleWAPictureUpdate(ctx, evt)
2024-09-10 15:56:51 +03:00
case *events.AppStateSyncComplete:
wa.handleWAAppStateSyncComplete(ctx, evt)
case *events.AppStateSyncError:
wa.handleWAAppStateSyncError(ctx, evt)
case *events.AppState:
// Intentionally ignored
2024-09-10 15:56:51 +03:00
case *events.PushNameSetting:
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
err := wa.updatePresence(ctx, types.PresenceUnavailable)
2024-09-10 15:56:51 +03:00
if err != nil {
log.Warn().Err(err).Msg("Failed to send presence after push name update")
}
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.JID.ToNonAD(), evt.Action.GetName())
2024-09-10 15:56:51 +03:00
if err != nil {
log.Err(err).Msg("Failed to update push name in store")
}
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName())
if err != nil {
log.Err(err).Msg("Failed to update push name in store")
}
go wa.syncGhost(wa.JID.ToNonAD(), "push name setting", nil)
case *events.Contact:
go wa.syncGhost(evt.JID, "contact event", nil)
case *events.PushName:
go wa.syncGhost(evt.JID, "push name event", nil)
case *events.BusinessName:
go wa.syncGhost(evt.JID, "business name event", nil)
case *events.Connected:
log.Debug().Msg("Connected to WhatsApp socket")
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
2025-03-06 15:53:29 +02:00
if len(wa.GetStore().PushName) > 0 {
go func() {
err := wa.updatePresence(ctx, types.PresenceUnavailable)
if err != nil {
log.Warn().Err(err).Msg("Failed to send initial presence after connecting")
}
}()
go wa.syncRemoteProfile(ctx, nil)
}
wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
case *events.OfflineSyncPreview:
log.Info().
Int("message_count", evt.Messages).
Int("receipt_count", evt.Receipts).
Int("notification_count", evt.Notifications).
Int("app_data_change_count", evt.AppDataChanges).
Msg("Server sent number of events that were missed during downtime")
case *events.OfflineSyncCompleted:
if !wa.PhoneRecentlySeen(true) {
log.Info().
Int("evt_count", evt.Count).
2024-09-16 15:38:39 +03:00
Time("phone_last_seen", wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PhoneLastSeen.Time).
Msg("Offline sync completed, but phone last seen date is still old")
} else {
log.Info().
Int("evt_count", evt.Count).
Msg("Offline sync completed")
}
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
wa.notifyOfflineSyncWaiter(nil)
case *events.LoggedOut:
wa.handleWALogout(evt.Reason, evt.OnConnect)
wa.notifyOfflineSyncWaiter(fmt.Errorf("logged out: %s", evt.Reason))
2024-08-13 14:11:10 +03:00
case *events.Disconnected:
2024-09-06 17:41:26 +03:00
// Don't send the normal transient disconnect state if we're already in a different transient disconnect state.
// TODO remove this if/when the phone offline state is moved to a sub-state of CONNECTED
if wa.UserLogin.BridgeState.GetPrev().Error != WAPhoneOffline && wa.PhoneRecentlySeen(false) {
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WADisconnected})
2024-08-13 14:11:10 +03:00
}
wa.notifyOfflineSyncWaiter(fmt.Errorf("disconnected"))
2024-09-06 17:41:26 +03:00
case *events.StreamError:
var message string
if evt.Code != "" {
message = fmt.Sprintf("Unknown stream error with code %s", evt.Code)
} else if children := evt.Raw.GetChildren(); len(children) > 0 {
message = fmt.Sprintf("Unknown stream error (contains %s node)", children[0].Tag)
} else {
message = "Unknown stream error"
2024-08-13 14:11:10 +03:00
}
2024-09-06 17:41:26 +03:00
wa.UserLogin.BridgeState.Send(status.BridgeState{
2024-08-13 14:11:10 +03:00
StateEvent: status.StateUnknownError,
2024-09-06 17:41:26 +03:00
Error: WAStreamError,
Message: message,
})
wa.notifyOfflineSyncWaiter(fmt.Errorf("stream error: %s", message))
2024-09-06 17:41:26 +03:00
case *events.StreamReplaced:
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced})
wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced"))
case *events.KeepAliveTimeout:
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout})
case *events.KeepAliveRestored:
log.Info().Msg("Keepalive restored after timeouts, sending connected event")
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
2024-09-06 17:41:26 +03:00
case *events.ConnectFailure:
wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateUnknownError,
Error: status.BridgeStateErrorCode(fmt.Sprintf("wa-connect-failure-%d", evt.Reason)),
Message: fmt.Sprintf("Unknown connection failure: %s (%s)", evt.Reason, evt.Message),
})
wa.notifyOfflineSyncWaiter(fmt.Errorf("connection failure: %s (%s)", evt.Reason, evt.Message))
2024-09-06 17:41:26 +03:00
case *events.ClientOutdated:
wa.UserLogin.Log.Error().Msg("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.")
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated})
wa.notifyOfflineSyncWaiter(fmt.Errorf("client outdated"))
2024-09-06 17:41:26 +03:00
case *events.TemporaryBan:
wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials,
Error: WATemporaryBan,
Message: evt.String(),
})
wa.notifyOfflineSyncWaiter(fmt.Errorf("temporary ban: %s", evt.String()))
2024-08-13 14:11:10 +03:00
default:
log.Debug().Type("event_type", rawEvt).Msg("Unhandled WhatsApp event")
}
return
2024-08-13 14:11:10 +03:00
}
func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) {
if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) &&
info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() {
info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender)
}
if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
}
if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Sender).
Stringer("pn", info.SenderAlt).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID DM sender to phone number in incoming message")
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
info.Chat = info.Sender.ToNonAD()
} else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Chat).
Stringer("pn", info.RecipientAlt).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID DM sender to phone number in own message sent from another device")
info.Chat = info.RecipientAlt.ToNonAD()
if info.Sender.Server == types.HiddenUserServer {
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
if info.Sender.IsEmpty() {
info.Sender = wa.GetStore().GetJID()
info.Sender.Device = info.SenderAlt.Device
}
}
} else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Sender).
Stringer("pn", info.SenderAlt).
Stringer("chat", info.Chat).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID broadcast list sender to phone number in incoming message")
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
} else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer {
2025-10-20 22:30:04 +03:00
chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
if err != nil {
wa.UserLogin.Log.Err(err).
Any("message_id", msgID).
Stringer("lid", info.Chat).
Str("evt_type", evtType).
Msg("Failed to get phone number of DM for incoming bot message")
} else if !chatPN.IsEmpty() {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Chat).
Stringer("pn", chatPN).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID chat to phone number in bot message")
info.Chat = chatPN
}
}
}
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
success = true
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Trace().
Any("info", evt.Info).
Any("payload", evt.Message).
Msg("Received WhatsApp message")
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return
}
if evt.Info.IsFromMe &&
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
wa.Main.Bridge.Config.Backfill.Enabled &&
wa.Client.ManualHistorySyncDownload {
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
}
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
parentKey := messageAssoc.GetParentMessageKey()
associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage()
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Str("parent_id", parentKey.GetID()).
Stringer("assoc_type", assocType).
Msg("Received HD replacement message, converting to edit")
protocolMsg := &waE2E.ProtocolMessage{
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
Key: parentKey,
EditedMessage: associatedMessage,
}
evt.Message = &waE2E.Message{
ProtocolMessage: protocolMsg,
}
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Str("parent_id", messageAssoc.GetParentMessageKey().GetID()).
Msg("Ignoring motion photo update")
return
}
2024-09-10 19:41:30 +03:00
parsedMessageType := getMessageType(evt.Message)
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
2024-09-10 19:41:30 +03:00
return
}
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
if err != nil {
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt reaction")
return
}
decrypted.Key = encReact.GetTargetMessageKey()
evt.Message.ReactionMessage = decrypted
}
if encComment := evt.Message.GetEncCommentMessage(); encComment != nil {
decrypted, err := wa.Client.DecryptComment(ctx, evt)
if err != nil {
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt comment")
} else {
decrypted.EncCommentMessage = evt.Message.GetEncCommentMessage()
evt.Message = decrypted
}
}
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
if err != nil {
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message")
return
}
evt.RawMessage = decrypted
evt.UnwrapRaw()
parsedMessageType = getMessageType(evt.Message)
}
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info,
wa: wa,
},
Message: evt.Message,
MsgEvent: evt,
2024-09-10 19:41:30 +03:00
parsedMessageType: parsedMessageType,
})
return res.Success
}
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Debug().
Any("info", evt.Info).
Bool("unavailable", evt.IsUnavailable).
Str("decrypt_fail", string(evt.DecryptFailMode)).
Msg("Received undecryptable WhatsApp message")
wa.trackUndecryptable(evt)
if evt.DecryptFailMode == events.DecryptFailHide {
return true
}
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return true
}
res := wa.UserLogin.QueueRemoteEvent(&WAUndecryptableMessage{
MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info,
wa: wa,
},
Type: evt.UnavailableType,
})
return res.Success
}
func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) {
origChat := evt.Chat
wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs)
if evt.IsFromMe && evt.Sender.Device == 0 {
wa.phoneSeen(evt.Timestamp)
}
var evtType bridgev2.RemoteEventType
switch evt.Type {
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
evtType = bridgev2.RemoteEventReadReceipt
case types.ReceiptTypeDelivered:
evtType = bridgev2.RemoteEventDeliveryReceipt
case types.ReceiptTypeSender:
fallthrough
default:
return true
}
targets := make([]networkid.MessageID, len(evt.MessageIDs))
messageSender := wa.JID
if !evt.MessageSender.IsEmpty() {
messageSender = evt.MessageSender
// Second part of rerouting receipts in LID chats
if messageSender == origChat && evt.Chat != origChat {
messageSender = evt.Chat
}
} else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer {
2025-10-20 22:30:04 +03:00
lid := wa.GetStore().GetLID()
if !lid.IsEmpty() {
messageSender = lid
}
}
for i, id := range evt.MessageIDs {
targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id)
}
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{
Type: evtType,
PortalKey: wa.makeWAPortalKey(evt.Chat),
Sender: wa.makeEventSender(ctx, evt.Sender),
Timestamp: evt.Timestamp,
},
Targets: targets,
})
return res.Success
}
func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) {
if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat {
if evt.SenderAlt.IsEmpty() {
evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender)
}
if evt.SenderAlt.Server == types.DefaultUserServer {
evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender
evt.Chat = evt.Sender.ToNonAD()
}
}
typingType := bridgev2.TypingTypeText
timeout := 15 * time.Second
if evt.Media == types.ChatPresenceMediaAudio {
typingType = bridgev2.TypingTypeRecordingMedia
}
if evt.State == types.ChatPresencePaused {
timeout = 0
}
wa.UserLogin.QueueRemoteEvent(&simplevent.Typing{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventTyping,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.Chat),
Sender: wa.makeEventSender(ctx, evt.Sender),
Timestamp: time.Now(),
},
Timeout: timeout,
Type: typingType,
})
}
func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onConnect bool) {
errorCode := WAUnknownLogout
if reason == events.ConnectFailureLoggedOut {
errorCode = WALoggedOut
} else if reason == events.ConnectFailureMainDeviceGone {
errorCode = WAMainDeviceGone
}
wa.Disconnect()
wa.Client = nil
wa.JID = types.EmptyJID
2024-09-16 15:38:39 +03:00
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials,
Error: errorCode,
})
}
const callEventMaxAge = 15 * time.Minute
func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender, senderAlt types.JID, id, callType string, ts time.Time) bool {
if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge {
return true
}
if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", sender).
Stringer("pn", senderAlt).
Str("call_id", id).
Msg("Forced LID caller to phone number in incoming call")
sender, senderAlt = senderAlt, sender
}
chat := group
if chat.IsEmpty() {
chat = sender
}
return wa.UserLogin.QueueRemoteEvent(&simplevent.Message[string]{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(chat),
Sender: wa.makeEventSender(ctx, sender),
CreatePortal: true,
Timestamp: ts,
StreamOrder: ts.Unix(),
},
Data: callType,
ID: waid.MakeFakeMessageID(chat, sender, "call-"+id),
ConvertMessageFunc: convertCallStart,
}).Success
}
func convertCallStart(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, callType string) (*bridgev2.ConvertedMessage, error) {
text := "Incoming call. Use the WhatsApp app to answer."
if callType != "" {
text = fmt.Sprintf("Incoming %s call. Use the WhatsApp app to answer.", callType)
}
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: text,
BeeperActionMessage: &event.BeeperActionMessage{
Type: event.BeeperActionMessageCall,
CallType: event.BeeperActionMessageCallType(callType),
},
},
}},
}, nil
}
func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *events.IdentityChange) {
if !wa.Main.Config.IdentityChangeNotices {
return
}
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.JID),
Sender: wa.makeEventSender(ctx, evt.JID),
CreatePortal: false,
Timestamp: evt.Timestamp,
},
Data: evt,
ID: waid.MakeFakeMessageID(evt.JID, evt.JID, "idchange-"+strconv.FormatInt(evt.Timestamp.UnixMilli(), 10)),
ConvertMessageFunc: convertIdentityChange,
})
}
func convertIdentityChange(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data *events.IdentityChange) (*bridgev2.ConvertedMessage, error) {
ghost, err := portal.Bridge.GetGhostByID(ctx, waid.MakeUserID(data.JID))
if err != nil {
return nil, err
}
text := fmt.Sprintf("Your security code with %s changed.", ghost.Name)
if data.Implicit {
text = fmt.Sprintf("Your security code with %s (device #%d) changed.", ghost.Name, data.JID.Device)
}
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: text,
},
}},
}, nil
}
func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.DeleteChat) bool {
chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID)
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatDelete,
PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: evt.Timestamp,
},
OnlyForMe: true,
Children: true,
}).Success
}
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessageRemove,
PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: evt.Timestamp,
},
TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID),
OnlyForMe: true,
}).Success
}
func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *events.MarkChatAsRead) bool {
chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID)
return wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReadReceipt,
PortalKey: wa.makeWAPortalKey(chatJID),
Sender: wa.makeEventSender(ctx, wa.JID),
Timestamp: evt.Timestamp,
},
ReadUpTo: evt.Timestamp,
}).Success
}
func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *string) {
log := wa.UserLogin.Log.With().
Str("action", "sync ghost").
Str("reason", reason).
Str("picture_id", ptr.Val(pictureID)).
Stringer("jid", jid).
Logger()
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
ghost, err := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
if err != nil {
log.Err(err).Msg("Failed to get ghost")
return
}
if pictureID != nil && *pictureID != "" && ghost.AvatarID == networkid.AvatarID(*pictureID) {
return
}
userInfo, err := wa.getUserInfo(ctx, jid, pictureID != nil)
if err != nil {
log.Err(err).Msg("Failed to get user info")
} else {
ghost.UpdateInfo(ctx, userInfo)
log.Debug().Msg("Synced ghost info")
wa.syncAltGhostWithInfo(ctx, jid, userInfo)
}
go wa.syncRemoteProfile(ctx, ghost)
}
func (wa *WhatsAppClient) handleWAPictureUpdate(ctx context.Context, evt *events.Picture) bool {
if evt.JID.Server == types.DefaultUserServer || evt.JID.Server == types.HiddenUserServer || evt.JID.Server == types.BotServer {
go wa.syncGhost(evt.JID, "picture event", &evt.PictureID)
return true
} else {
var changes bridgev2.ChatInfo
if evt.Remove {
changes.Avatar = &bridgev2.Avatar{Remove: true, ID: "remove"}
} else {
changes.ExtraUpdates = wa.makePortalAvatarFetcher(evt.PictureID, evt.Author, evt.Timestamp)
}
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.
Str("wa_event_type", "picture").
Stringer("picture_author", evt.Author).
Str("new_picture_id", evt.PictureID).
Bool("remove_picture", evt.Remove)
},
PortalKey: wa.makeWAPortalKey(evt.JID),
Sender: wa.makeEventSender(ctx, evt.Author),
Timestamp: evt.Timestamp,
},
ChatInfoChange: &bridgev2.ChatInfoChange{
ChatInfo: &changes,
},
}).Success
}
}
func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *events.GroupInfo) bool {
eventMeta := simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.JID),
CreatePortal: true,
Timestamp: evt.Timestamp,
}
if evt.Sender != nil {
eventMeta.Sender = wa.makeEventSender(ctx, *evt.Sender)
}
if evt.Delete != nil {
eventMeta.Type = bridgev2.RemoteEventChatDelete
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{EventMeta: eventMeta}).Success
} else {
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: eventMeta,
ChatInfoChange: wa.wrapGroupInfoChange(ctx, evt),
}).Success
}
}
func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool {
if wa.createDedup.Pop(evt.CreateKey) {
return true
}
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.JID),
CreatePortal: true,
},
ChatInfo: wa.wrapGroupInfo(ctx, &evt.GroupInfo),
}).Success
}
func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool {
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.ID),
CreatePortal: true,
},
ChatInfo: wa.wrapNewsletterInfo(ctx, &evt.NewsletterMetadata),
}).Success
}
func (wa *WhatsAppClient) handleWANewsletterLeave(evt *events.NewsletterLeave) bool {
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatDelete,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.ID),
},
OnlyForMe: true,
}).Success
}
func (wa *WhatsAppClient) handleWAUserLocalPortalInfo(chatJID types.JID, ts time.Time, info *bridgev2.UserLocalPortalInfo) bool {
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: ts,
},
ChatInfoChange: &bridgev2.ChatInfoChange{
ChatInfo: &bridgev2.ChatInfo{
UserLocal: info,
},
},
}).Success
}
func (wa *WhatsAppClient) handleWAMute(evt *events.Mute) bool {
var mutedUntil time.Time
if evt.Action.GetMuted() {
mutedUntil = event.MutedForever
2025-10-22 13:04:21 +03:00
if evt.Action.GetMuteEndTimestamp() > 0 {
mutedUntil = time.Unix(evt.Action.GetMuteEndTimestamp(), 0)
}
} else {
mutedUntil = bridgev2.Unmuted
}
return wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
MutedUntil: &mutedUntil,
})
}
func (wa *WhatsAppClient) handleWAArchive(evt *events.Archive) bool {
var tag event.RoomTag
if evt.Action.GetArchived() {
tag = wa.Main.Config.ArchiveTag
}
return wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
Tag: &tag,
})
}
func (wa *WhatsAppClient) handleWAPin(evt *events.Pin) bool {
var tag event.RoomTag
if evt.Action.GetPinned() {
tag = wa.Main.Config.PinnedTag
}
return wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
Tag: &tag,
})
}
func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt *events.AppStateSyncComplete) {
log := zerolog.Ctx(ctx).With().
Str("patch_name", string(evt.Name)).
Uint64("patch_version", evt.Version).
Logger()
if len(wa.GetStore().PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
err := wa.updatePresence(ctx, types.PresenceUnavailable)
if err != nil {
log.Warn().Err(err).Msg("Failed to send presence after app state sync")
}
go wa.syncRemoteProfile(log.WithContext(context.Background()), nil)
} else if evt.Name == appstate.WAPatchCriticalUnblockLow {
go wa.resyncContacts(false, true)
}
wa.appStateRecoveryLock.Lock()
defer wa.appStateRecoveryLock.Unlock()
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists {
delete(wa.appStateFullSyncAttempted, evt.Name)
delete(meta.AppStateRecoveryAttempted, evt.Name)
err := wa.UserLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to save login metadata after unmarking app state recovery as attempted")
} else {
log.Info().
Time("recovery_ts", ts).
Msg("Unmarked app state recovery as attempted after successful full sync")
}
} else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists {
delete(wa.appStateFullSyncAttempted, evt.Name)
log.Debug().Time("full_sync_ts", ts).Msg("Unmarked app state full sync attempted after successful sync")
}
}
func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *events.AppStateSyncError) {
log := zerolog.Ctx(ctx).With().
Str("patch_name", string(evt.Name)).
Logger()
wa.appStateRecoveryLock.Lock()
defer wa.appStateRecoveryLock.Unlock()
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
lastRecovery := meta.AppStateRecoveryAttempted[evt.Name]
lastFullSync := wa.appStateFullSyncAttempted[evt.Name]
if !lastRecovery.IsZero() && time.Since(lastRecovery) < 48*time.Hour {
log.Debug().Err(evt.Error).
Time("last_recovery_attempt", lastRecovery).
Time("last_full_sync_attempt", lastFullSync).
Msg("App state sync failed, but recovery already attempted")
return
}
if !evt.FullSync {
if !lastFullSync.IsZero() {
log.Debug().
Err(evt.Error).
Time("last_full_sync_attempt", lastFullSync).
Msg("App state sync failed, but full sync already attempted")
return
}
wa.appStateFullSyncAttempted[evt.Name] = time.Now()
log.Info().
Err(evt.Error).
Msg("Trying full sync for app state after partial sync error")
go func() {
err := wa.Client.FetchAppState(ctx, evt.Name, true, false)
if err != nil {
log.Err(err).Msg("Full app state sync failed")
} else {
log.Debug().Msg("Full app state sync succeeded")
}
}()
return
}
log.Info().
Err(evt.Error).
Msg("Trying recovery for app state after full sync error")
if meta.AppStateRecoveryAttempted == nil {
meta.AppStateRecoveryAttempted = make(map[appstate.WAPatchName]time.Time)
}
meta.AppStateRecoveryAttempted[evt.Name] = time.Now()
err := wa.UserLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted")
}
go func() {
resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name))
if err != nil {
log.Err(err).Msg("Failed to send app state recovery request")
} else {
log.Debug().
Str("message_id", resp.ID).
Time("message_ts", resp.Timestamp).
Msg("Sent app state recovery request")
}
}()
}