// Copyright (c) 2021 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package events contains all the events that whatsmeow.Client emits to functions registered with AddEventHandler. package events import ( "fmt" "strconv" "time" waBinary "go.mau.fi/whatsmeow/binary" armadillo "go.mau.fi/whatsmeow/proto" "go.mau.fi/whatsmeow/proto/instamadilloTransportPayload" "go.mau.fi/whatsmeow/proto/waArmadilloApplication" "go.mau.fi/whatsmeow/proto/waConsumerApplication" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waHistorySync" "go.mau.fi/whatsmeow/proto/waMsgApplication" "go.mau.fi/whatsmeow/proto/waMsgTransport" "go.mau.fi/whatsmeow/proto/waWeb" "go.mau.fi/whatsmeow/types" ) // QR is emitted after connecting when there's no session data in the device store. // // The QR codes are available in the Codes slice. You should render the strings as QR codes one by // one, switching to the next one whenever enough time has passed. WhatsApp web seems to show the // first code for 60 seconds and all other codes for 20 seconds. // // When the QR code has been scanned and pairing is complete, PairSuccess will be emitted. If you // run out of codes before scanning, the server will close the websocket, and you will have to // reconnect to get more codes. type QR struct { Codes []string } // PairSuccess is emitted after the QR code has been scanned with the phone and the handshake has // been completed. Note that this is generally followed by a websocket reconnection, so you should // wait for the Connected before trying to send anything. type PairSuccess struct { ID types.JID LID types.JID BusinessName string Platform string } // PairError is emitted when a pair-success event is received from the server, but finishing the pairing locally fails. type PairError struct { ID types.JID LID types.JID BusinessName string Platform string Error error } // QRScannedWithoutMultidevice is emitted when the pairing QR code is scanned, but the phone didn't have multidevice enabled. // The same QR code can still be scanned after this event, which means the user can just be told to enable multidevice and re-scan the code. type QRScannedWithoutMultidevice struct{} // Connected is emitted when the client has successfully connected to the WhatsApp servers // and is authenticated. The user who the client is authenticated as will be in the device store // at this point, which is why this event doesn't contain any data. type Connected struct{} // KeepAliveTimeout is emitted when the keepalive ping request to WhatsApp web servers times out. // // Currently, there's no automatic handling for these, but it's expected that the TCP connection will // either start working again or notice it's dead on its own eventually. Clients may use this event to // decide to force a disconnect+reconnect faster. type KeepAliveTimeout struct { ErrorCount int LastSuccess time.Time } // KeepAliveRestored is emitted if the keepalive pings start working again after some KeepAliveTimeout events. // Note that if the websocket disconnects before the pings start working, this event will not be emitted. type KeepAliveRestored struct{} // PermanentDisconnect is a class of events emitted when the client will not auto-reconnect by default. type PermanentDisconnect interface { PermanentDisconnectDescription() string } func (l *LoggedOut) PermanentDisconnectDescription() string { return l.Reason.String() } func (*StreamReplaced) PermanentDisconnectDescription() string { return "stream replaced" } func (*ClientOutdated) PermanentDisconnectDescription() string { return "client outdated" } func (*CATRefreshError) PermanentDisconnectDescription() string { return "CAT refresh failed" } func (tb *TemporaryBan) PermanentDisconnectDescription() string { return fmt.Sprintf("temporarily banned: %s", tb.String()) } func (cf *ConnectFailure) PermanentDisconnectDescription() string { return fmt.Sprintf("connect failure: %s", cf.Reason.String()) } // LoggedOut is emitted when the client has been unpaired from the phone. // // This can happen while connected (stream:error messages) or right after connecting (connect failure messages). // // This will not be emitted when the logout is initiated by this client (using Client.LogOut()). type LoggedOut struct { // OnConnect is true if the event was triggered by a connect failure message. // If it's false, the event was triggered by a stream:error message. OnConnect bool // If OnConnect is true, then this field contains the reason code. Reason ConnectFailureReason } // StreamReplaced is emitted when the client is disconnected by another client connecting with the same keys. // // This can happen if you accidentally start another process with the same session // or otherwise try to connect twice with the same session. type StreamReplaced struct{} // ManualLoginReconnect is emitted after login if DisableLoginAutoReconnect is set. type ManualLoginReconnect struct{} // TempBanReason is an error code included in temp ban error events. type TempBanReason int const ( TempBanSentToTooManyPeople TempBanReason = 101 TempBanBlockedByUsers TempBanReason = 102 TempBanCreatedTooManyGroups TempBanReason = 103 TempBanSentTooManySameMessage TempBanReason = 104 TempBanBroadcastList TempBanReason = 106 ) var tempBanReasonMessage = map[TempBanReason]string{ TempBanSentToTooManyPeople: "you sent too many messages to people who don't have you in their address books", TempBanBlockedByUsers: "too many people blocked you", TempBanCreatedTooManyGroups: "you created too many groups with people who don't have you in their address books", TempBanSentTooManySameMessage: "you sent the same message to too many people", TempBanBroadcastList: "you sent too many messages to a broadcast list", } // String returns the reason code and a human-readable description of the ban reason. func (tbr TempBanReason) String() string { msg, ok := tempBanReasonMessage[tbr] if !ok { msg = "you may have violated the terms of service (unknown error)" } return fmt.Sprintf("%d: %s", int(tbr), msg) } // TemporaryBan is emitted when there's a connection failure with the ConnectFailureTempBanned reason code. type TemporaryBan struct { Code TempBanReason Expire time.Duration } func (tb *TemporaryBan) String() string { if tb.Expire == 0 { return fmt.Sprintf("You've been temporarily banned: %v", tb.Code) } return fmt.Sprintf("You've been temporarily banned: %v. The ban expires in %v", tb.Code, tb.Expire) } // ConnectFailureReason is an error code included in connection failure events. type ConnectFailureReason int const ( ConnectFailureGeneric ConnectFailureReason = 400 ConnectFailureLoggedOut ConnectFailureReason = 401 ConnectFailureTempBanned ConnectFailureReason = 402 ConnectFailureMainDeviceGone ConnectFailureReason = 403 // this is now called LOCKED in the whatsapp web code ConnectFailureUnknownLogout ConnectFailureReason = 406 // this is now called BANNED in the whatsapp web code ConnectFailureClientOutdated ConnectFailureReason = 405 ConnectFailureBadUserAgent ConnectFailureReason = 409 ConnectFailureCATExpired ConnectFailureReason = 413 ConnectFailureCATInvalid ConnectFailureReason = 414 ConnectFailureNotFound ConnectFailureReason = 415 // Status code unknown (not in WA web) ConnectFailureClientUnknown ConnectFailureReason = 418 ConnectFailureInternalServerError ConnectFailureReason = 500 ConnectFailureExperimental ConnectFailureReason = 501 ConnectFailureServiceUnavailable ConnectFailureReason = 503 ) var connectFailureReasonMessage = map[ConnectFailureReason]string{ ConnectFailureLoggedOut: "logged out from another device", ConnectFailureTempBanned: "account temporarily banned", ConnectFailureMainDeviceGone: "primary device was logged out", // seems to happen for both bans and switching phones ConnectFailureUnknownLogout: "logged out for unknown reason", ConnectFailureClientOutdated: "client is out of date", ConnectFailureBadUserAgent: "client user agent was rejected", ConnectFailureCATExpired: "messenger crypto auth token has expired", ConnectFailureCATInvalid: "messenger crypto auth token is invalid", } // IsLoggedOut returns true if the client should delete session data due to this connect failure. func (cfr ConnectFailureReason) IsLoggedOut() bool { return cfr == ConnectFailureLoggedOut || cfr == ConnectFailureMainDeviceGone || cfr == ConnectFailureUnknownLogout } func (cfr ConnectFailureReason) NumberString() string { return strconv.Itoa(int(cfr)) } // String returns the reason code and a short human-readable description of the error. func (cfr ConnectFailureReason) String() string { msg, ok := connectFailureReasonMessage[cfr] if !ok { msg = "unknown error" } return fmt.Sprintf("%d: %s", int(cfr), msg) } // ConnectFailure is emitted when the WhatsApp server sends a node with an unknown reason. // // Known reasons are handled internally and emitted as different events (e.g. LoggedOut and TemporaryBan). type ConnectFailure struct { Reason ConnectFailureReason Message string Raw *waBinary.Node } // ClientOutdated is emitted when the WhatsApp server rejects the connection with the ConnectFailureClientOutdated code. type ClientOutdated struct{} type CATRefreshError struct { Error error } // StreamError is emitted when the WhatsApp server sends a node with an unknown code. // // Known codes are handled internally and emitted as different events (e.g. LoggedOut). type StreamError struct { Code string Raw *waBinary.Node } // Disconnected is emitted when the websocket is closed by the server. type Disconnected struct{} // HistorySync is emitted when the phone has sent a blob of historical messages. type HistorySync struct { Data *waHistorySync.HistorySync } type DecryptFailMode string const ( DecryptFailShow DecryptFailMode = "" DecryptFailHide DecryptFailMode = "hide" ) type UnavailableType string const ( UnavailableTypeUnknown UnavailableType = "" UnavailableTypeViewOnce UnavailableType = "view_once" ) // UndecryptableMessage is emitted when receiving a new message that failed to decrypt. // // The library will automatically ask the sender to retry. If the sender resends the message, // and it's decryptable, then it will be emitted as a normal Message event. // // The UndecryptableMessage event may also be repeated if the resent message is also undecryptable. type UndecryptableMessage struct { Info types.MessageInfo // IsUnavailable is true if the recipient device didn't send a ciphertext to this device at all // (as opposed to sending a ciphertext, but the ciphertext not being decryptable). IsUnavailable bool // Some message types are intentionally unavailable. Such types usually have a type specified here. UnavailableType UnavailableType DecryptFailMode DecryptFailMode } type NewsletterMessageMeta struct { // When a newsletter message is edited, the message isn't wrapped in an EditedMessage like normal messages. // Instead, the message is the new content, the ID is the original message ID, and the edit timestamp is here. EditTS time.Time // This is the timestamp of the original message for edits. OriginalTS time.Time } // Message is emitted when receiving a new message. type Message struct { Info types.MessageInfo // Information about the message like the chat and sender IDs Message *waE2E.Message // The actual message struct IsEphemeral bool // True if the message was unwrapped from an EphemeralMessage IsViewOnce bool // True if the message was unwrapped from a ViewOnceMessage, ViewOnceMessageV2 or ViewOnceMessageV2Extension IsViewOnceV2 bool // True if the message was unwrapped from a ViewOnceMessageV2 or ViewOnceMessageV2Extension IsViewOnceV2Extension bool // True if the message was unwrapped from a ViewOnceMessageV2Extension IsDocumentWithCaption bool // True if the message was unwrapped from a DocumentWithCaptionMessage IsLottieSticker bool // True if the message was unwrapped from a LottieStickerMessage IsBotInvoke bool // True if the message was unwrapped from a BotInvokeMessage IsEdit bool // True if the message was unwrapped from an EditedMessage // If this event was parsed from a WebMessageInfo (i.e. from a history sync or unavailable message request), the source data is here. SourceWebMsg *waWeb.WebMessageInfo // If this event is a response to an unavailable message request, the request ID is here. UnavailableRequestID types.MessageID // If the message was re-requested from the sender, this is the number of retries it took. RetryCount int NewsletterMeta *NewsletterMessageMeta // The raw message struct. This is the raw unmodified data, which means the actual message might // be wrapped in DeviceSentMessage, EphemeralMessage or ViewOnceMessage. RawMessage *waE2E.Message } type FBMessage struct { Info types.MessageInfo // Information about the message like the chat and sender IDs Message armadillo.MessageApplicationSub // The actual message struct // If the message was re-requested from the sender, this is the number of retries it took. RetryCount int Transport *waMsgTransport.MessageTransport // The first level of wrapping the message was in FBApplication *waMsgApplication.MessageApplication // The second level of wrapping the message was in, for FB messages IGTransport *instamadilloTransportPayload.TransportPayload // The second level of wrapping the message was in, for IG messages } func (evt *FBMessage) GetConsumerApplication() *waConsumerApplication.ConsumerApplication { if consumerApp, ok := evt.Message.(*waConsumerApplication.ConsumerApplication); ok { return consumerApp } return nil } func (evt *FBMessage) GetArmadillo() *waArmadilloApplication.Armadillo { if armadillo, ok := evt.Message.(*waArmadilloApplication.Armadillo); ok { return armadillo } return nil } // UnwrapRaw fills the Message, IsEphemeral and IsViewOnce fields based on the raw message in the RawMessage field. func (evt *Message) UnwrapRaw() *Message { evt.Message = evt.RawMessage if evt.Message.GetDeviceSentMessage().GetMessage() != nil { evt.Info.DeviceSentMeta = &types.DeviceSentMeta{ DestinationJID: evt.Message.GetDeviceSentMessage().GetDestinationJID(), Phash: evt.Message.GetDeviceSentMessage().GetPhash(), } evt.Message = evt.Message.GetDeviceSentMessage().GetMessage() } if evt.Message.GetBotInvokeMessage().GetMessage() != nil { evt.Message = evt.Message.GetBotInvokeMessage().GetMessage() evt.IsBotInvoke = true } if evt.Message.GetEphemeralMessage().GetMessage() != nil { evt.Message = evt.Message.GetEphemeralMessage().GetMessage() evt.IsEphemeral = true } if evt.Message.GetViewOnceMessage().GetMessage() != nil { evt.Message = evt.Message.GetViewOnceMessage().GetMessage() evt.IsViewOnce = true } if evt.Message.GetViewOnceMessageV2().GetMessage() != nil { evt.Message = evt.Message.GetViewOnceMessageV2().GetMessage() evt.IsViewOnce = true evt.IsViewOnceV2 = true } if evt.Message.GetViewOnceMessageV2Extension().GetMessage() != nil { evt.Message = evt.Message.GetViewOnceMessageV2Extension().GetMessage() evt.IsViewOnce = true evt.IsViewOnceV2 = true evt.IsViewOnceV2Extension = true } if evt.Message.GetLottieStickerMessage().GetMessage() != nil { evt.Message = evt.Message.GetLottieStickerMessage().GetMessage() evt.IsLottieSticker = true } if evt.Message.GetDocumentWithCaptionMessage().GetMessage() != nil { evt.Message = evt.Message.GetDocumentWithCaptionMessage().GetMessage() evt.IsDocumentWithCaption = true } if evt.Message.GetEditedMessage().GetMessage() != nil { evt.Message = evt.Message.GetEditedMessage().GetMessage() evt.IsEdit = true } if evt.Message != nil && evt.RawMessage != nil && evt.Message.MessageContextInfo == nil && evt.RawMessage.MessageContextInfo != nil { evt.Message.MessageContextInfo = evt.RawMessage.MessageContextInfo } return evt } // Deprecated: use types.ReceiptType directly type ReceiptType = types.ReceiptType // Deprecated: use types.ReceiptType* constants directly const ( ReceiptTypeDelivered = types.ReceiptTypeDelivered ReceiptTypeSender = types.ReceiptTypeSender ReceiptTypeRetry = types.ReceiptTypeRetry ReceiptTypeRead = types.ReceiptTypeRead ReceiptTypeReadSelf = types.ReceiptTypeReadSelf ReceiptTypePlayed = types.ReceiptTypePlayed ) // Receipt is emitted when an outgoing message is delivered to or read by another user, or when another device reads an incoming message. // // N.B. WhatsApp on Android sends message IDs from newest message to oldest, but WhatsApp on iOS sends them in the opposite order (oldest first). type Receipt struct { types.MessageSource MessageIDs []types.MessageID Timestamp time.Time Type types.ReceiptType // When you read the message of another user in a group, this field contains the sender of the message. // For receipts from other users, the message sender is always you. MessageSender types.JID } // ChatPresence is emitted when a chat state update (also known as typing notification) is received. // // Note that WhatsApp won't send you these updates unless you mark yourself as online: // // client.SendPresence(types.PresenceAvailable) type ChatPresence struct { types.MessageSource State types.ChatPresence // The current state, either composing or paused Media types.ChatPresenceMedia // When composing, the type of message } // Presence is emitted when a presence update is received. // // Note that WhatsApp only sends you presence updates for individual users after you subscribe to them: // // client.SubscribePresence(user JID) type Presence struct { // The user whose presence event this is From types.JID // True if the user is now offline Unavailable bool // The time when the user was last online. This may be the zero value if the user has hid their last seen time. LastSeen time.Time } // JoinedGroup is emitted when you join or are added to a group. type JoinedGroup struct { Reason string // If the event was triggered by you using an invite link, this will be "invite". Type string // "new" if it's a newly created group. CreateKey types.MessageID // If you created the group, this is the same message ID you passed to CreateGroup. // For type new, the user who created the group and added you to it Sender *types.JID SenderPN *types.JID Notify string types.GroupInfo } // GroupInfo is emitted when the metadata of a group changes. type GroupInfo struct { JID types.JID // The group ID in question Notify string // Seems like a top-level type for the invite Sender *types.JID // The user who made the change. Doesn't seem to be present when notify=invite SenderPN *types.JID // The phone number of the user who made the change, if Sender is a LID. Timestamp time.Time // The time when the change occurred Name *types.GroupName // Group name change Topic *types.GroupTopic // Group topic (description) change Locked *types.GroupLocked // Group locked status change (can only admins edit group info?) Announce *types.GroupAnnounce // Group announce status change (can only admins send messages?) Ephemeral *types.GroupEphemeral // Disappearing messages change MembershipApprovalMode *types.GroupMembershipApprovalMode // Membership approval mode change Delete *types.GroupDelete Link *types.GroupLinkChange Unlink *types.GroupLinkChange NewInviteLink *string // Group invite link change PrevParticipantVersionID string ParticipantVersionID string JoinReason string // This will be "invite" if the user joined via invite link Join []types.JID // Users who joined or were added the group Leave []types.JID // Users who left or were removed from the group Promote []types.JID // Users who were promoted to admins Demote []types.JID // Users who were demoted to normal users Suspended bool // whether the group is suspended Unsuspended bool // whether the group is unsuspended UnknownChanges []*waBinary.Node } // Picture is emitted when a user's profile picture or group's photo is changed. // // You can use Client.GetProfilePictureInfo to get the actual image URL after this event. type Picture struct { JID types.JID // The user or group ID where the picture was changed. Author types.JID // The user who changed the picture. Timestamp time.Time // The timestamp when the picture was changed. Remove bool // True if the picture was removed. PictureID string // The new picture ID if it was not removed. } // UserAbout is emitted when a user's about status is changed. type UserAbout struct { JID types.JID // The user whose status was changed Status string // The new status Timestamp time.Time // The timestamp when the status was changed. } // IdentityChange is emitted when another user changes their primary device. type IdentityChange struct { JID types.JID Timestamp time.Time // Implicit will be set to true if the event was triggered by an untrusted identity error, // rather than an identity change notification from the server. Implicit bool } // PrivacySettings is emitted when the user changes their privacy settings. type PrivacySettings struct { NewSettings types.PrivacySettings GroupAddChanged bool LastSeenChanged bool StatusChanged bool ProfileChanged bool ReadReceiptsChanged bool OnlineChanged bool CallAddChanged bool } // OfflineSyncPreview is emitted right after connecting if the server is going to send events that the client missed during downtime. type OfflineSyncPreview struct { Total int AppDataChanges int Messages int Notifications int Receipts int } // OfflineSyncCompleted is emitted after the server has finished sending missed events. type OfflineSyncCompleted struct { Count int } type MediaRetryError struct { Code int } // MediaRetry is emitted when the phone sends a response to a media retry request. type MediaRetry struct { Ciphertext []byte IV []byte // Sometimes there's an unencrypted media retry error. In these cases, Ciphertext and IV will be nil. Error *MediaRetryError Timestamp time.Time // The time of the response. MessageID types.MessageID // The ID of the message. ChatID types.JID // The chat ID where the message was sent. SenderID types.JID // The user who sent the message. Only present in groups. FromMe bool // Whether the message was sent by the current user or someone else. } type BlocklistAction string const ( BlocklistActionDefault BlocklistAction = "" BlocklistActionModify BlocklistAction = "modify" ) // Blocklist is emitted when the user's blocked user list is changed. type Blocklist struct { // Action specifies what happened. If it's empty, there should be a list of changes in the Changes list. // If it's "modify", then the Changes list will be empty and the whole blocklist should be re-requested. Action BlocklistAction DHash string PrevDHash string Changes []BlocklistChange } type BlocklistChangeAction string const ( BlocklistChangeActionBlock BlocklistChangeAction = "block" BlocklistChangeActionUnblock BlocklistChangeAction = "unblock" ) type BlocklistChange struct { JID types.JID Action BlocklistChangeAction } type NewsletterJoin struct { types.NewsletterMetadata } type NewsletterLeave struct { ID types.JID `json:"id"` Role types.NewsletterRole `json:"role"` } type NewsletterMuteChange struct { ID types.JID `json:"id"` Mute types.NewsletterMuteState `json:"mute"` } type NewsletterLiveUpdate struct { JID types.JID Time time.Time Messages []*types.NewsletterMessage }