4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-07-03 07:17:44 +00:00

Update dependencies (#1841)

This commit is contained in:
Wim
2022-06-11 23:07:42 +02:00
committed by GitHub
parent 3819062574
commit 8751fb4bb1
188 changed files with 5608 additions and 1334 deletions

View File

@ -6,6 +6,12 @@ whatsmeow is a Go library for the WhatsApp web multidevice API.
## Discussion
Matrix room: [#whatsmeow:maunium.net](https://matrix.to/#/#whatsmeow:maunium.net)
For questions about the WhatsApp protocol (like how to send a specific type of
message), you can also use the [WhatsApp protocol Q&A] section on GitHub
discussions.
[WhatsApp protocol Q&A]: https://github.com/tulir/whatsmeow/discussions/categories/whatsapp-protocol-q-a
## Usage
The [godoc](https://pkg.go.dev/go.mau.fi/whatsmeow) includes docs for all methods and event types.
There's also a [simple example](https://godocs.io/go.mau.fi/whatsmeow#example-package) at the top.
@ -23,9 +29,10 @@ Most core features are already present:
* Sending and receiving delivery and read receipts
* Reading app state (contact list, chat pin/mute status, etc)
* Sending and handling retry receipts if message decryption fails
* Sending status messages (experimental, may not work for large contact lists)
Things that are not yet implemented:
* Writing app state (contact list, chat pin/mute status, etc)
* Sending status messages or broadcast list messages (this is not supported on WhatsApp web either)
* Sending broadcast list messages (this is not supported on WhatsApp web either)
* Calls

View File

@ -7,6 +7,8 @@
package whatsmeow
import (
"encoding/hex"
"errors"
"fmt"
"time"
@ -53,6 +55,9 @@ func (cli *Client) FetchAppState(name appstate.WAPatchName, fullSync, onlyIfNotS
mutations, newState, err := cli.appStateProc.DecodePatches(patches, state, true)
if err != nil {
if errors.Is(err, appstate.ErrKeyNotFound) {
go cli.requestMissingAppStateKeys(patches)
}
return fmt.Errorf("failed to decode app state %s patches: %w", name, err)
}
wasFullSync := state.Version == 0 && patches.Snapshot != nil
@ -228,3 +233,42 @@ func (cli *Client) fetchAppStatePatches(name appstate.WAPatchName, fromVersion u
}
return appstate.ParsePatchList(resp, cli.downloadExternalAppStateBlob)
}
func (cli *Client) requestMissingAppStateKeys(patches *appstate.PatchList) {
cli.appStateKeyRequestsLock.Lock()
rawKeyIDs := cli.appStateProc.GetMissingKeyIDs(patches)
filteredKeyIDs := make([][]byte, 0, len(rawKeyIDs))
now := time.Now()
for _, keyID := range rawKeyIDs {
stringKeyID := hex.EncodeToString(keyID)
lastRequestTime := cli.appStateKeyRequests[stringKeyID]
if lastRequestTime.IsZero() || lastRequestTime.Add(24*time.Hour).Before(now) {
cli.appStateKeyRequests[stringKeyID] = now
filteredKeyIDs = append(filteredKeyIDs, keyID)
}
}
cli.appStateKeyRequestsLock.Unlock()
cli.requestAppStateKeys(filteredKeyIDs)
}
func (cli *Client) requestAppStateKeys(rawKeyIDs [][]byte) {
keyIDs := make([]*waProto.AppStateSyncKeyId, len(rawKeyIDs))
debugKeyIDs := make([]string, len(rawKeyIDs))
for i, keyID := range rawKeyIDs {
keyIDs[i] = &waProto.AppStateSyncKeyId{KeyId: keyID}
debugKeyIDs[i] = hex.EncodeToString(keyID)
}
msg := &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST.Enum(),
AppStateSyncKeyRequest: &waProto.AppStateSyncKeyRequest{
KeyIds: keyIDs,
},
},
}
cli.Log.Infof("Sending key request for app state keys %+v", debugKeyIDs)
_, err := cli.SendMessage(cli.Store.ID.ToNonAD(), "", msg)
if err != nil {
cli.Log.Warnf("Failed to send app state key request: %v", err)
}
}

View File

@ -83,3 +83,36 @@ func (proc *Processor) getAppStateKey(keyID []byte) (keys ExpandedAppStateKeys,
}
return
}
func (proc *Processor) GetMissingKeyIDs(pl *PatchList) [][]byte {
cache := make(map[string]bool)
var missingKeys [][]byte
checkMissing := func(keyID []byte) {
if keyID == nil {
return
}
stringKeyID := base64.RawStdEncoding.EncodeToString(keyID)
_, alreadyAdded := cache[stringKeyID]
if !alreadyAdded {
keyData, err := proc.Store.AppStateKeys.GetAppStateSyncKey(keyID)
if err != nil {
proc.Log.Warnf("Error fetching key %X while checking if it's missing: %v", keyID, err)
}
missing := keyData == nil && err == nil
cache[stringKeyID] = missing
if missing {
missingKeys = append(missingKeys, keyID)
}
}
}
if pl.Snapshot != nil {
checkMissing(pl.Snapshot.GetKeyId().GetId())
for _, record := range pl.Snapshot.GetRecords() {
checkMissing(record.GetKeyId().GetId())
}
}
for _, patch := range pl.Patches {
checkMissing(patch.GetKeyId().GetId())
}
return missingKeys
}

View File

@ -9,6 +9,7 @@ package binary
import (
"fmt"
"strconv"
"time"
"go.mau.fi/whatsmeow/types"
)
@ -112,6 +113,16 @@ func (au *AttrUtility) GetBool(key string, require bool) (bool, bool) {
}
}
func (au *AttrUtility) GetUnixTime(key string, require bool) (time.Time, bool) {
if intVal, ok := au.GetInt64(key, require); !ok {
return time.Time{}, false
} else if intVal == 0 {
return time.Time{}, true
} else {
return time.Unix(intVal, 0), true
}
}
// OptionalString returns the string under the given key.
func (au *AttrUtility) OptionalString(key string) string {
strVal, _ := au.GetString(key, false)
@ -155,6 +166,16 @@ func (au *AttrUtility) Bool(key string) bool {
return val
}
func (au *AttrUtility) OptionalUnixTime(key string) time.Time {
val, _ := au.GetUnixTime(key, false)
return val
}
func (au *AttrUtility) UnixTime(key string) time.Time {
val, _ := au.GetUnixTime(key, true)
return val
}
// OK returns true if there are no errors.
func (au *AttrUtility) OK() bool {
return len(au.Errors) == 0

View File

@ -2739,7 +2739,7 @@ func (x *DNSSource_DNSSourceDNSResolutionMethod) UnmarshalJSON(b []byte) error {
// Deprecated: Use DNSSource_DNSSourceDNSResolutionMethod.Descriptor instead.
func (DNSSource_DNSSourceDNSResolutionMethod) EnumDescriptor() ([]byte, []int) {
return file_binary_proto_def_proto_rawDescGZIP(), []int{182, 0}
return file_binary_proto_def_proto_rawDescGZIP(), []int{183, 0}
}
type WebMessageInfo_WebMessageInfoStatus int32
@ -10131,7 +10131,6 @@ type ContextInfo struct {
ActionLink *ActionLink `protobuf:"bytes,33,opt,name=actionLink" json:"actionLink,omitempty"`
GroupSubject *string `protobuf:"bytes,34,opt,name=groupSubject" json:"groupSubject,omitempty"`
ParentGroupJid *string `protobuf:"bytes,35,opt,name=parentGroupJid" json:"parentGroupJid,omitempty"`
MessageSecret []byte `protobuf:"bytes,36,opt,name=messageSecret" json:"messageSecret,omitempty"`
}
func (x *ContextInfo) Reset() {
@ -10327,13 +10326,6 @@ func (x *ContextInfo) GetParentGroupJid() string {
return ""
}
func (x *ContextInfo) GetMessageSecret() []byte {
if x != nil {
return x.MessageSecret
}
return nil
}
type ExternalAdReplyInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -11577,6 +11569,7 @@ type MessageContextInfo struct {
DeviceListMetadata *DeviceListMetadata `protobuf:"bytes,1,opt,name=deviceListMetadata" json:"deviceListMetadata,omitempty"`
DeviceListMetadataVersion *int32 `protobuf:"varint,2,opt,name=deviceListMetadataVersion" json:"deviceListMetadataVersion,omitempty"`
MessageSecret []byte `protobuf:"bytes,3,opt,name=messageSecret" json:"messageSecret,omitempty"`
}
func (x *MessageContextInfo) Reset() {
@ -11625,6 +11618,13 @@ func (x *MessageContextInfo) GetDeviceListMetadataVersion() int32 {
return 0
}
func (x *MessageContextInfo) GetMessageSecret() []byte {
if x != nil {
return x.MessageSecret
}
return nil
}
type VideoMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -13220,6 +13220,8 @@ type GlobalSettings struct {
AutoDownloadRoaming *AutoDownloadSettings `protobuf:"bytes,6,opt,name=autoDownloadRoaming" json:"autoDownloadRoaming,omitempty"`
ShowIndividualNotificationsPreview *bool `protobuf:"varint,7,opt,name=showIndividualNotificationsPreview" json:"showIndividualNotificationsPreview,omitempty"`
ShowGroupNotificationsPreview *bool `protobuf:"varint,8,opt,name=showGroupNotificationsPreview" json:"showGroupNotificationsPreview,omitempty"`
DisappearingModeDuration *int32 `protobuf:"varint,9,opt,name=disappearingModeDuration" json:"disappearingModeDuration,omitempty"`
DisappearingModeTimestamp *int64 `protobuf:"varint,10,opt,name=disappearingModeTimestamp" json:"disappearingModeTimestamp,omitempty"`
}
func (x *GlobalSettings) Reset() {
@ -13310,6 +13312,20 @@ func (x *GlobalSettings) GetShowGroupNotificationsPreview() bool {
return false
}
func (x *GlobalSettings) GetDisappearingModeDuration() int32 {
if x != nil && x.DisappearingModeDuration != nil {
return *x.DisappearingModeDuration
}
return 0
}
func (x *GlobalSettings) GetDisappearingModeTimestamp() int64 {
if x != nil && x.DisappearingModeTimestamp != nil {
return *x.DisappearingModeTimestamp
}
return 0
}
type Conversation struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -17690,7 +17706,7 @@ type ClientPayload struct {
DnsSource *DNSSource `protobuf:"bytes,15,opt,name=dnsSource" json:"dnsSource,omitempty"`
ConnectAttemptCount *uint32 `protobuf:"varint,16,opt,name=connectAttemptCount" json:"connectAttemptCount,omitempty"`
Device *uint32 `protobuf:"varint,18,opt,name=device" json:"device,omitempty"`
RegData *CompanionRegData `protobuf:"bytes,19,opt,name=regData" json:"regData,omitempty"`
DevicePairingData *DevicePairingRegistrationData `protobuf:"bytes,19,opt,name=devicePairingData" json:"devicePairingData,omitempty"`
Product *ClientPayload_ClientPayloadProduct `protobuf:"varint,20,opt,name=product,enum=proto.ClientPayload_ClientPayloadProduct" json:"product,omitempty"`
FbCat []byte `protobuf:"bytes,21,opt,name=fbCat" json:"fbCat,omitempty"`
FbUserAgent []byte `protobuf:"bytes,22,opt,name=fbUserAgent" json:"fbUserAgent,omitempty"`
@ -17825,9 +17841,9 @@ func (x *ClientPayload) GetDevice() uint32 {
return 0
}
func (x *ClientPayload) GetRegData() *CompanionRegData {
func (x *ClientPayload) GetDevicePairingData() *DevicePairingRegistrationData {
if x != nil {
return x.RegData
return x.DevicePairingData
}
return nil
}
@ -18236,6 +18252,109 @@ func (x *UserAgent) GetDeviceBoard() string {
return ""
}
type DevicePairingRegistrationData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ERegid []byte `protobuf:"bytes,1,opt,name=eRegid" json:"eRegid,omitempty"`
EKeytype []byte `protobuf:"bytes,2,opt,name=eKeytype" json:"eKeytype,omitempty"`
EIdent []byte `protobuf:"bytes,3,opt,name=eIdent" json:"eIdent,omitempty"`
ESkeyId []byte `protobuf:"bytes,4,opt,name=eSkeyId" json:"eSkeyId,omitempty"`
ESkeyVal []byte `protobuf:"bytes,5,opt,name=eSkeyVal" json:"eSkeyVal,omitempty"`
ESkeySig []byte `protobuf:"bytes,6,opt,name=eSkeySig" json:"eSkeySig,omitempty"`
BuildHash []byte `protobuf:"bytes,7,opt,name=buildHash" json:"buildHash,omitempty"`
DeviceProps []byte `protobuf:"bytes,8,opt,name=deviceProps" json:"deviceProps,omitempty"`
}
func (x *DevicePairingRegistrationData) Reset() {
*x = DevicePairingRegistrationData{}
if protoimpl.UnsafeEnabled {
mi := &file_binary_proto_def_proto_msgTypes[182]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DevicePairingRegistrationData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DevicePairingRegistrationData) ProtoMessage() {}
func (x *DevicePairingRegistrationData) ProtoReflect() protoreflect.Message {
mi := &file_binary_proto_def_proto_msgTypes[182]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DevicePairingRegistrationData.ProtoReflect.Descriptor instead.
func (*DevicePairingRegistrationData) Descriptor() ([]byte, []int) {
return file_binary_proto_def_proto_rawDescGZIP(), []int{182}
}
func (x *DevicePairingRegistrationData) GetERegid() []byte {
if x != nil {
return x.ERegid
}
return nil
}
func (x *DevicePairingRegistrationData) GetEKeytype() []byte {
if x != nil {
return x.EKeytype
}
return nil
}
func (x *DevicePairingRegistrationData) GetEIdent() []byte {
if x != nil {
return x.EIdent
}
return nil
}
func (x *DevicePairingRegistrationData) GetESkeyId() []byte {
if x != nil {
return x.ESkeyId
}
return nil
}
func (x *DevicePairingRegistrationData) GetESkeyVal() []byte {
if x != nil {
return x.ESkeyVal
}
return nil
}
func (x *DevicePairingRegistrationData) GetESkeySig() []byte {
if x != nil {
return x.ESkeySig
}
return nil
}
func (x *DevicePairingRegistrationData) GetBuildHash() []byte {
if x != nil {
return x.BuildHash
}
return nil
}
func (x *DevicePairingRegistrationData) GetDeviceProps() []byte {
if x != nil {
return x.DeviceProps
}
return nil
}
type DNSSource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -18248,7 +18367,7 @@ type DNSSource struct {
func (x *DNSSource) Reset() {
*x = DNSSource{}
if protoimpl.UnsafeEnabled {
mi := &file_binary_proto_def_proto_msgTypes[182]
mi := &file_binary_proto_def_proto_msgTypes[183]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -18261,7 +18380,7 @@ func (x *DNSSource) String() string {
func (*DNSSource) ProtoMessage() {}
func (x *DNSSource) ProtoReflect() protoreflect.Message {
mi := &file_binary_proto_def_proto_msgTypes[182]
mi := &file_binary_proto_def_proto_msgTypes[183]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -18274,7 +18393,7 @@ func (x *DNSSource) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSSource.ProtoReflect.Descriptor instead.
func (*DNSSource) Descriptor() ([]byte, []int) {
return file_binary_proto_def_proto_rawDescGZIP(), []int{182}
return file_binary_proto_def_proto_rawDescGZIP(), []int{183}
}
func (x *DNSSource) GetDnsMethod() DNSSource_DNSSourceDNSResolutionMethod {
@ -18291,109 +18410,6 @@ func (x *DNSSource) GetAppCached() bool {
return false
}
type CompanionRegData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ERegid []byte `protobuf:"bytes,1,opt,name=eRegid" json:"eRegid,omitempty"`
EKeytype []byte `protobuf:"bytes,2,opt,name=eKeytype" json:"eKeytype,omitempty"`
EIdent []byte `protobuf:"bytes,3,opt,name=eIdent" json:"eIdent,omitempty"`
ESkeyId []byte `protobuf:"bytes,4,opt,name=eSkeyId" json:"eSkeyId,omitempty"`
ESkeyVal []byte `protobuf:"bytes,5,opt,name=eSkeyVal" json:"eSkeyVal,omitempty"`
ESkeySig []byte `protobuf:"bytes,6,opt,name=eSkeySig" json:"eSkeySig,omitempty"`
BuildHash []byte `protobuf:"bytes,7,opt,name=buildHash" json:"buildHash,omitempty"`
CompanionProps []byte `protobuf:"bytes,8,opt,name=companionProps" json:"companionProps,omitempty"`
}
func (x *CompanionRegData) Reset() {
*x = CompanionRegData{}
if protoimpl.UnsafeEnabled {
mi := &file_binary_proto_def_proto_msgTypes[183]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CompanionRegData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CompanionRegData) ProtoMessage() {}
func (x *CompanionRegData) ProtoReflect() protoreflect.Message {
mi := &file_binary_proto_def_proto_msgTypes[183]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CompanionRegData.ProtoReflect.Descriptor instead.
func (*CompanionRegData) Descriptor() ([]byte, []int) {
return file_binary_proto_def_proto_rawDescGZIP(), []int{183}
}
func (x *CompanionRegData) GetERegid() []byte {
if x != nil {
return x.ERegid
}
return nil
}
func (x *CompanionRegData) GetEKeytype() []byte {
if x != nil {
return x.EKeytype
}
return nil
}
func (x *CompanionRegData) GetEIdent() []byte {
if x != nil {
return x.EIdent
}
return nil
}
func (x *CompanionRegData) GetESkeyId() []byte {
if x != nil {
return x.ESkeyId
}
return nil
}
func (x *CompanionRegData) GetESkeyVal() []byte {
if x != nil {
return x.ESkeyVal
}
return nil
}
func (x *CompanionRegData) GetESkeySig() []byte {
if x != nil {
return x.ESkeySig
}
return nil
}
func (x *CompanionRegData) GetBuildHash() []byte {
if x != nil {
return x.BuildHash
}
return nil
}
func (x *CompanionRegData) GetCompanionProps() []byte {
if x != nil {
return x.CompanionProps
}
return nil
}
type WebNotificationsInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -20122,8 +20138,8 @@ var file_binary_proto_def_proto_goTypes = []interface{}{
(*WebInfo)(nil), // 229: proto.WebInfo
(*WebdPayload)(nil), // 230: proto.WebdPayload
(*UserAgent)(nil), // 231: proto.UserAgent
(*DNSSource)(nil), // 232: proto.DNSSource
(*CompanionRegData)(nil), // 233: proto.CompanionRegData
(*DevicePairingRegistrationData)(nil), // 232: proto.DevicePairingRegistrationData
(*DNSSource)(nil), // 233: proto.DNSSource
(*WebNotificationsInfo)(nil), // 234: proto.WebNotificationsInfo
(*WebMessageInfo)(nil), // 235: proto.WebMessageInfo
(*WebFeatures)(nil), // 236: proto.WebFeatures
@ -20417,8 +20433,8 @@ var file_binary_proto_def_proto_depIdxs = []int32{
229, // 276: proto.ClientPayload.webInfo:type_name -> proto.WebInfo
35, // 277: proto.ClientPayload.connectType:type_name -> proto.ClientPayload.ClientPayloadConnectType
36, // 278: proto.ClientPayload.connectReason:type_name -> proto.ClientPayload.ClientPayloadConnectReason
232, // 279: proto.ClientPayload.dnsSource:type_name -> proto.DNSSource
233, // 280: proto.ClientPayload.regData:type_name -> proto.CompanionRegData
233, // 279: proto.ClientPayload.dnsSource:type_name -> proto.DNSSource
232, // 280: proto.ClientPayload.devicePairingData:type_name -> proto.DevicePairingRegistrationData
37, // 281: proto.ClientPayload.product:type_name -> proto.ClientPayload.ClientPayloadProduct
38, // 282: proto.ClientPayload.iosAppExtension:type_name -> proto.ClientPayload.ClientPayloadIOSAppExtension
230, // 283: proto.WebInfo.webdPayload:type_name -> proto.WebdPayload
@ -22698,7 +22714,7 @@ func file_binary_proto_def_proto_init() {
}
}
file_binary_proto_def_proto_msgTypes[182].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DNSSource); i {
switch v := v.(*DevicePairingRegistrationData); i {
case 0:
return &v.state
case 1:
@ -22710,7 +22726,7 @@ func file_binary_proto_def_proto_init() {
}
}
file_binary_proto_def_proto_msgTypes[183].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CompanionRegData); i {
switch v := v.(*DNSSource); i {
case 0:
return &v.state
case 1:

View File

@ -781,7 +781,6 @@ message ContextInfo {
optional ActionLink actionLink = 33;
optional string groupSubject = 34;
optional string parentGroupJid = 35;
optional bytes messageSecret = 36;
}
message ExternalAdReplyInfo {
@ -932,6 +931,7 @@ message Message {
message MessageContextInfo {
optional DeviceListMetadata deviceListMetadata = 1;
optional int32 deviceListMetadataVersion = 2;
optional bytes messageSecret = 3;
}
message VideoMessage {
@ -1123,6 +1123,8 @@ message GlobalSettings {
optional AutoDownloadSettings autoDownloadRoaming = 6;
optional bool showIndividualNotificationsPreview = 7;
optional bool showGroupNotificationsPreview = 8;
optional int32 disappearingModeDuration = 9;
optional int64 disappearingModeTimestamp = 10;
}
message Conversation {
@ -1633,7 +1635,7 @@ message ClientPayload {
optional DNSSource dnsSource = 15;
optional uint32 connectAttemptCount = 16;
optional uint32 device = 18;
optional CompanionRegData regData = 19;
optional DevicePairingRegistrationData devicePairingData = 19;
enum ClientPayloadProduct {
WHATSAPP = 0;
MESSENGER = 1;
@ -1744,6 +1746,17 @@ message UserAgent {
// optional uint32 quinary = 5;
//}
message DevicePairingRegistrationData {
optional bytes eRegid = 1;
optional bytes eKeytype = 2;
optional bytes eIdent = 3;
optional bytes eSkeyId = 4;
optional bytes eSkeyVal = 5;
optional bytes eSkeySig = 6;
optional bytes buildHash = 7;
optional bytes deviceProps = 8;
}
message DNSSource {
enum DNSSourceDNSResolutionMethod {
SYSTEM = 0;
@ -1756,17 +1769,6 @@ message DNSSource {
optional bool appCached = 16;
}
message CompanionRegData {
optional bytes eRegid = 1;
optional bytes eKeytype = 2;
optional bytes eIdent = 3;
optional bytes eSkeyId = 4;
optional bytes eSkeyVal = 5;
optional bytes eSkeySig = 6;
optional bytes buildHash = 7;
optional bytes companionProps = 8;
}
message WebNotificationsInfo {
optional uint64 timestamp = 2;
optional uint32 unreadChats = 3;

138
vendor/go.mau.fi/whatsmeow/broadcast.go vendored Normal file
View File

@ -0,0 +1,138 @@
// Copyright (c) 2022 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 whatsmeow
import (
"errors"
"fmt"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
func (cli *Client) getBroadcastListParticipants(jid types.JID) ([]types.JID, error) {
var list []types.JID
var err error
if jid == types.StatusBroadcastJID {
list, err = cli.getStatusBroadcastRecipients()
} else {
return nil, ErrBroadcastListUnsupported
}
if err != nil {
return nil, err
}
var hasSelf bool
for _, participant := range list {
if participant.User == cli.Store.ID.User {
hasSelf = true
break
}
}
if !hasSelf {
list = append(list, cli.Store.ID.ToNonAD())
}
return list, nil
}
func (cli *Client) getStatusBroadcastRecipients() ([]types.JID, error) {
statusPrivacyOptions, err := cli.GetStatusPrivacy()
if err != nil {
return nil, fmt.Errorf("failed to get status privacy: %w", err)
}
statusPrivacy := statusPrivacyOptions[0]
if statusPrivacy.Type == types.StatusPrivacyTypeWhitelist {
// Whitelist mode, just return the list
return statusPrivacy.List, nil
}
// Blacklist or all contacts mode. Find all contacts from database, then filter them appropriately.
contacts, err := cli.Store.Contacts.GetAllContacts()
if err != nil {
return nil, fmt.Errorf("failed to get contact list from db: %w", err)
}
blacklist := make(map[types.JID]struct{})
if statusPrivacy.Type == types.StatusPrivacyTypeBlacklist {
for _, jid := range statusPrivacy.List {
blacklist[jid] = struct{}{}
}
}
var contactsArray []types.JID
for jid, contact := range contacts {
_, isBlacklisted := blacklist[jid]
if isBlacklisted {
continue
}
// TODO should there be a better way to separate contacts and found push names in the db?
if len(contact.FullName) > 0 {
contactsArray = append(contactsArray, jid)
}
}
return contactsArray, nil
}
var DefaultStatusPrivacy = []types.StatusPrivacy{{
Type: types.StatusPrivacyTypeContacts,
IsDefault: true,
}}
// GetStatusPrivacy gets the user's status privacy settings (who to send status broadcasts to).
//
// There can be multiple different stored settings, the first one is always the default.
func (cli *Client) GetStatusPrivacy() ([]types.StatusPrivacy, error) {
resp, err := cli.sendIQ(infoQuery{
Namespace: "status",
Type: iqGet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "privacy",
}},
})
if err != nil {
if errors.Is(err, ErrIQNotFound) {
return DefaultStatusPrivacy, nil
}
return nil, err
}
privacyLists := resp.GetChildByTag("privacy")
var outputs []types.StatusPrivacy
for _, list := range privacyLists.GetChildren() {
if list.Tag != "list" {
continue
}
ag := list.AttrGetter()
var out types.StatusPrivacy
out.IsDefault = ag.OptionalBool("default")
out.Type = types.StatusPrivacyType(ag.String("type"))
children := list.GetChildren()
if len(children) > 0 {
out.List = make([]types.JID, 0, len(children))
for _, child := range children {
jid, ok := child.Attrs["jid"].(types.JID)
if child.Tag == "user" && ok {
out.List = append(out.List, jid)
}
}
}
outputs = append(outputs, out)
if out.IsDefault {
// Move default to always be first in the list
outputs[len(outputs)-1] = outputs[0]
outputs[0] = out
}
if len(ag.Errors) > 0 {
return nil, ag.Error()
}
}
if len(outputs) == 0 {
return DefaultStatusPrivacy, nil
}
return outputs, nil
}

View File

@ -7,8 +7,6 @@
package whatsmeow
import (
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
@ -26,7 +24,7 @@ func (cli *Client) handleCallEvent(node *waBinary.Node) {
cag := child.AttrGetter()
basicMeta := types.BasicCallMeta{
From: ag.JID("from"),
Timestamp: time.Unix(ag.Int64("t"), 0),
Timestamp: ag.UnixTime("t"),
CallCreator: cag.JID("call-creator"),
CallID: cag.String("call-id"),
}

View File

@ -10,7 +10,6 @@ package whatsmeow
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
@ -52,6 +51,7 @@ type Client struct {
socket *socket.NoiseSocket
socketLock sync.RWMutex
socketWait chan struct{}
isLoggedIn uint32
expectedDisconnectVal uint32
@ -88,6 +88,11 @@ type Client struct {
messageRetries map[string]int
messageRetriesLock sync.Mutex
appStateKeyRequests map[string]time.Time
appStateKeyRequestsLock sync.RWMutex
messageSendLock sync.Mutex
privacySettingsCache atomic.Value
groupParticipantsCache map[types.JID][]types.JID
@ -99,22 +104,21 @@ type Client struct {
recentMessagesList [recentMessagesSize]recentMessageKey
recentMessagesPtr int
recentMessagesLock sync.RWMutex
sessionRecreateHistory map[types.JID]time.Time
sessionRecreateHistoryLock sync.Mutex
// GetMessageForRetry is used to find the source message for handling retry receipts
// when the message is not found in the recently sent message cache.
GetMessageForRetry func(to types.JID, id types.MessageID) *waProto.Message
GetMessageForRetry func(requester, to types.JID, id types.MessageID) *waProto.Message
// PreRetryCallback is called before a retry receipt is accepted.
// If it returns false, the accepting will be cancelled and the retry receipt will be ignored.
PreRetryCallback func(receipt *events.Receipt, retryCount int, msg *waProto.Message) bool
PreRetryCallback func(receipt *events.Receipt, id types.MessageID, retryCount int, msg *waProto.Message) bool
// Should untrusted identity errors be handled automatically? If true, the stored identity and existing signal
// sessions will be removed on untrusted identity errors, and an events.IdentityChange will be dispatched.
// If false, decrypting a message from untrusted devices will fail.
AutoTrustIdentity bool
DebugDecodeBeforeSend bool
OneMessageAtATime bool
messageSendLock sync.Mutex
uniqueID string
idCounter uint32
@ -162,14 +166,17 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
messageRetries: make(map[string]int),
handlerQueue: make(chan *waBinary.Node, handlerQueueSize),
appStateProc: appstate.NewProcessor(deviceStore, log.Sub("AppState")),
socketWait: make(chan struct{}),
historySyncNotifications: make(chan *waProto.HistorySyncNotification, 32),
groupParticipantsCache: make(map[types.JID][]types.JID),
userDevicesCache: make(map[types.JID][]types.JID),
recentMessagesMap: make(map[recentMessageKey]*waProto.Message, recentMessagesSize),
GetMessageForRetry: func(to types.JID, id types.MessageID) *waProto.Message { return nil },
recentMessagesMap: make(map[recentMessageKey]*waProto.Message, recentMessagesSize),
sessionRecreateHistory: make(map[types.JID]time.Time),
GetMessageForRetry: func(requester, to types.JID, id types.MessageID) *waProto.Message { return nil },
appStateKeyRequests: make(map[string]time.Time),
EnableAutoReconnect: true,
AutoTrustIdentity: true,
@ -226,6 +233,37 @@ func (cli *Client) SetProxy(proxy socket.Proxy) {
cli.http.Transport.(*http.Transport).Proxy = proxy
}
func (cli *Client) getSocketWaitChan() <-chan struct{} {
cli.socketLock.RLock()
ch := cli.socketWait
cli.socketLock.RUnlock()
return ch
}
func (cli *Client) closeSocketWaitChan() {
cli.socketLock.Lock()
close(cli.socketWait)
cli.socketWait = make(chan struct{})
cli.socketLock.Unlock()
}
func (cli *Client) WaitForConnection(timeout time.Duration) bool {
timeoutChan := time.After(timeout)
cli.socketLock.RLock()
for cli.socket == nil || !cli.socket.IsConnected() || !cli.IsLoggedIn() {
ch := cli.socketWait
cli.socketLock.RUnlock()
select {
case <-ch:
case <-timeoutChan:
return false
}
cli.socketLock.RLock()
}
cli.socketLock.RUnlock()
return true
}
// Connect connects the client to the WhatsApp web websocket. After connection, it will either
// authenticate if there's data in the device store, or emit a QREvent to set up a new link.
func (cli *Client) Connect() error {
@ -322,6 +360,9 @@ func (cli *Client) IsConnected() bool {
}
// Disconnect disconnects from the WhatsApp web websocket.
//
// This will not emit any events, the Disconnected event is only used when the
// connection is closed by the server or a network error.
func (cli *Client) Disconnect() {
if cli.socket == nil {
return
@ -336,6 +377,7 @@ func (cli *Client) unlockedDisconnect() {
if cli.socket != nil {
cli.socket.Stop(true)
cli.socket = nil
cli.clearResponseWaiters(xmlStreamEndNode)
}
}
@ -343,6 +385,9 @@ func (cli *Client) unlockedDisconnect() {
//
// If the logout request fails, the disconnection and local data deletion will not happen either.
// If an error is returned, but you want to force disconnect/clear data, call Client.Disconnect() and Client.Store.Delete() manually.
//
// Note that this will not emit any events. The LoggedOut event is only used for external logouts
// (triggered by the user from the main device or by WhatsApp servers).
func (cli *Client) Logout() error {
if cli.Store.ID == nil {
return ErrNotLoggedIn
@ -491,7 +536,7 @@ func (cli *Client) handlerQueueLoop(ctx context.Context) {
}
}
func (cli *Client) sendNodeDebug(node waBinary.Node) ([]byte, error) {
func (cli *Client) sendNodeAndGetData(node waBinary.Node) ([]byte, error) {
cli.socketLock.RLock()
sock := cli.socket
cli.socketLock.RUnlock()
@ -503,22 +548,13 @@ func (cli *Client) sendNodeDebug(node waBinary.Node) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("failed to marshal node: %w", err)
}
if cli.DebugDecodeBeforeSend {
var decoded *waBinary.Node
decoded, err = waBinary.Unmarshal(payload[1:])
if err != nil {
cli.Log.Infof("Malformed payload: %s", base64.URLEncoding.EncodeToString(payload))
return nil, fmt.Errorf("failed to decode the binary we just produced: %w", err)
}
node = *decoded
}
cli.sendLog.Debugf("%s", node.XMLString())
return payload, sock.SendFrame(payload)
}
func (cli *Client) sendNode(node waBinary.Node) error {
_, err := cli.sendNodeDebug(node)
_, err := cli.sendNodeAndGetData(node)
return err
}
@ -535,3 +571,45 @@ func (cli *Client) dispatchEvent(evt interface{}) {
handler.fn(evt)
}
}
// ParseWebMessage parses a WebMessageInfo object into *events.Message to match what real-time messages have.
//
// The chat JID can be found in the Conversation data:
// chatJID, err := types.ParseJID(conv.GetId())
// for _, historyMsg := range conv.GetMessages() {
// evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
// yourNormalEventHandler(evt)
// }
func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessageInfo) (*events.Message, error) {
info := types.MessageInfo{
MessageSource: types.MessageSource{
Chat: chatJID,
IsFromMe: webMsg.GetKey().GetFromMe(),
IsGroup: chatJID.Server == types.GroupServer,
},
ID: webMsg.GetKey().GetId(),
PushName: webMsg.GetPushName(),
Timestamp: time.Unix(int64(webMsg.GetMessageTimestamp()), 0),
}
var err error
if info.IsFromMe {
info.Sender = cli.Store.ID.ToNonAD()
} else if chatJID.Server == types.DefaultUserServer {
info.Sender = chatJID
} else if webMsg.GetParticipant() != "" {
info.Sender, err = types.ParseJID(webMsg.GetParticipant())
} else if webMsg.GetKey().GetParticipant() != "" {
info.Sender, err = types.ParseJID(webMsg.GetKey().GetParticipant())
} else {
return nil, fmt.Errorf("couldn't find sender of message %s", info.ID)
}
if err != nil {
return nil, fmt.Errorf("failed to parse sender of message %s: %v", info.ID, err)
}
evt := &events.Message{
RawMessage: webMsg.GetMessage(),
Info: info,
}
evt.UnwrapRaw()
return evt, nil
}

View File

@ -89,11 +89,7 @@ func (cli *Client) handleConnectFailure(node *waBinary.Node) {
}
} else if reason == events.ConnectFailureTempBanned {
cli.Log.Warnf("Temporary ban connect failure: %s", node.XMLString())
expiryTimeUnix := ag.Int64("expire")
var expiryTime time.Time
if expiryTimeUnix > 0 {
expiryTime = time.Unix(expiryTimeUnix, 0)
}
expiryTime := ag.UnixTime("expire")
go cli.dispatchEvent(&events.TemporaryBan{
Code: events.TempBanReason(ag.Int("code")),
Expire: expiryTime,
@ -130,6 +126,7 @@ func (cli *Client) handleConnectSuccess(node *waBinary.Node) {
cli.Log.Warnf("Failed to send post-connect passive IQ: %v", err)
}
cli.dispatchEvent(&events.Connected{})
cli.closeSocketWaitChan()
}()
}

View File

@ -13,6 +13,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
@ -183,11 +184,20 @@ func (cli *Client) Download(msg DownloadableMessage) ([]byte, error) {
return nil, fmt.Errorf("%w '%s'", ErrUnknownMediaType, string(msg.ProtoReflect().Descriptor().Name()))
}
urlable, ok := msg.(downloadableMessageWithURL)
if ok && len(urlable.GetUrl()) > 0 {
var url string
var isWebWhatsappNetURL bool
if ok {
url = urlable.GetUrl()
isWebWhatsappNetURL = strings.HasPrefix(urlable.GetUrl(), "https://web.whatsapp.net")
}
if len(url) > 0 && !isWebWhatsappNetURL {
return cli.downloadAndDecrypt(urlable.GetUrl(), msg.GetMediaKey(), mediaType, getSize(msg), msg.GetFileEncSha256(), msg.GetFileSha256())
} else if len(msg.GetDirectPath()) > 0 {
return cli.DownloadMediaWithPath(msg.GetDirectPath(), msg.GetFileEncSha256(), msg.GetFileSha256(), msg.GetMediaKey(), getSize(msg), mediaType, mediaTypeToMMSType[mediaType])
} else {
if isWebWhatsappNetURL {
cli.Log.Warnf("Got a media message with a web.whatsapp.net URL (%s) and no direct path", url)
}
return nil, ErrNoURLPresent
}
}

View File

@ -50,11 +50,13 @@ var (
ErrMediaNotAvailableOnPhone = errors.New("media no longer available on phone")
// ErrUnknownMediaRetryError is returned by DecryptMediaRetryNotification if the given event contains an unknown error code.
ErrUnknownMediaRetryError = errors.New("unknown media retry error")
// ErrInvalidDisappearingTimer is returned by SetDisappearingTimer if the given timer is not one of the allowed values.
ErrInvalidDisappearingTimer = errors.New("invalid disappearing timer provided")
)
// Some errors that Client.SendMessage can return
var (
ErrBroadcastListUnsupported = errors.New("sending to broadcast lists is not yet supported")
ErrBroadcastListUnsupported = errors.New("sending to non-status broadcast lists is not yet supported")
ErrUnknownServer = errors.New("can't send message to unknown server")
ErrRecipientADJID = errors.New("message recipient must be normal (non-AD) JID")
)
@ -104,6 +106,7 @@ type IQError struct {
// Common errors returned by info queries for use with errors.Is
var (
ErrIQBadRequest error = &IQError{Code: 400, Text: "bad-request"}
ErrIQNotAuthorized error = &IQError{Code: 401, Text: "not-authorized"}
ErrIQForbidden error = &IQError{Code: 403, Text: "forbidden"}
ErrIQNotFound error = &IQError{Code: 404, Text: "item-not-found"}

View File

@ -10,7 +10,6 @@ import (
"errors"
"fmt"
"strings"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
@ -397,10 +396,10 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
group.OwnerJID = ag.OptionalJIDOrEmpty("creator")
group.Name = ag.String("subject")
group.NameSetAt = time.Unix(ag.Int64("s_t"), 0)
group.NameSetAt = ag.UnixTime("s_t")
group.NameSetBy = ag.OptionalJIDOrEmpty("s_o")
group.GroupCreated = time.Unix(ag.Int64("creation"), 0)
group.GroupCreated = ag.UnixTime("creation")
group.AnnounceVersionID = ag.OptionalString("a_v_id")
group.ParticipantVersionID = ag.OptionalString("p_v_id")
@ -423,7 +422,7 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
group.Topic = string(topicBytes)
group.TopicID = childAG.String("id")
group.TopicSetBy = childAG.OptionalJIDOrEmpty("participant")
group.TopicSetAt = time.Unix(childAG.Int64("t"), 0)
group.TopicSetAt = childAG.UnixTime("t")
}
case "announcement":
group.IsAnnounce = true
@ -477,7 +476,7 @@ func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, err
evt.JID = ag.JID("from")
evt.Notify = ag.OptionalString("notify")
evt.Sender = ag.OptionalJID("participant")
evt.Timestamp = time.Unix(ag.Int64("t"), 0)
evt.Timestamp = ag.UnixTime("t")
if !ag.OK() {
return nil, fmt.Errorf("group change doesn't contain required attributes: %w", ag.Error())
}
@ -505,7 +504,7 @@ func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, err
case "subject":
evt.Name = &types.GroupName{
Name: cag.String("subject"),
NameSetAt: time.Unix(cag.Int64("s_t"), 0),
NameSetAt: cag.UnixTime("s_t"),
NameSetBy: cag.OptionalJIDOrEmpty("s_o"),
}
case "description":

View File

@ -53,3 +53,11 @@ func (int *DangerousInternalClient) RefreshMediaConn(force bool) (*MediaConn, er
func (int *DangerousInternalClient) GetServerPreKeyCount() (int, error) {
return int.c.getServerPreKeyCount()
}
func (int *DangerousInternalClient) RequestAppStateKeys(keyIDs [][]byte) {
int.c.requestAppStateKeys(keyIDs)
}
func (int *DangerousInternalClient) SendRetryReceipt(node *waBinary.Node, forceIncludeIdentity bool) {
int.c.sendRetryReceipt(node, forceIncludeIdentity)
}

View File

@ -13,6 +13,7 @@ import (
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
var (
@ -25,12 +26,27 @@ var (
)
func (cli *Client) keepAliveLoop(ctx context.Context) {
var lastSuccess time.Time
var errorCount int
for {
interval := rand.Int63n(KeepAliveIntervalMax.Milliseconds()-KeepAliveIntervalMin.Milliseconds()) + KeepAliveIntervalMin.Milliseconds()
select {
case <-time.After(time.Duration(interval) * time.Millisecond):
if !cli.sendKeepAlive(ctx) {
isSuccess, shouldContinue := cli.sendKeepAlive(ctx)
if !shouldContinue {
return
} else if !isSuccess {
errorCount++
go cli.dispatchEvent(&events.KeepAliveTimeout{
ErrorCount: errorCount,
LastSuccess: lastSuccess,
})
} else {
if errorCount > 0 {
errorCount = 0
go cli.dispatchEvent(&events.KeepAliveRestored{})
}
lastSuccess = time.Now()
}
case <-ctx.Done():
return
@ -38,7 +54,7 @@ func (cli *Client) keepAliveLoop(ctx context.Context) {
}
}
func (cli *Client) sendKeepAlive(ctx context.Context) bool {
func (cli *Client) sendKeepAlive(ctx context.Context) (isSuccess, shouldContinue bool) {
respCh, err := cli.sendIQAsync(infoQuery{
Namespace: "w:p",
Type: "get",
@ -47,16 +63,16 @@ func (cli *Client) sendKeepAlive(ctx context.Context) bool {
})
if err != nil {
cli.Log.Warnf("Failed to send keepalive: %v", err)
return true
return false, true
}
select {
case <-respCh:
// All good
return true, true
case <-time.After(KeepAliveResponseDeadline):
// TODO disconnect websocket?
cli.Log.Warnf("Keepalive timed out")
return false, true
case <-ctx.Done():
return false
return false, false
}
return true
}

View File

@ -11,7 +11,6 @@ import (
"crypto/cipher"
"crypto/rand"
"fmt"
"time"
"google.golang.org/protobuf/proto"
@ -64,8 +63,38 @@ func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphe
// SendMediaRetryReceipt sends a request to the phone to re-upload the media in a message.
//
// This is mostly relevant when handling history syncs and getting a 404 or 410 error downloading media.
// Rough example on how to use it (will not work out of the box, you must adjust it depending on what you need exactly):
//
// var mediaRetryCache map[types.MessageID]*waProto.ImageMessage
//
// evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
// imageMsg := evt.Message.GetImageMessage() // replace this with the part of the message you want to download
// data, err := cli.Download(imageMsg)
// if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
// err = cli.SendMediaRetryReceipt(&evt.Info, imageMsg.GetMediaKey())
// // You need to store the event data somewhere as it's necessary for handling the retry response.
// mediaRetryCache[evt.Info.ID] = imageMsg
// }
//
// The response will come as an *events.MediaRetry. The response will then have to be decrypted
// using DecryptMediaRetryNotification and the same media key passed here.
// using DecryptMediaRetryNotification and the same media key passed here. If the media retry was successful,
// the decrypted notification should contain an updated DirectPath, which can be used to download the file.
//
// func eventHandler(rawEvt interface{}) {
// switch evt := rawEvt.(type) {
// case *events.MediaRetry:
// imageMsg := mediaRetryCache[evt.MessageID]
// retryData, err := whatsmeow.DecryptMediaRetryNotification(evt, imageMsg.GetMediaKey())
// if err != nil || retryData.GetResult != waProto.MediaRetryNotification_SUCCESS {
// return
// }
// // Use the new path to download the attachment
// imageMsg.DirectPath = retryData.DirectPath
// data, err := cli.Download(imageMsg)
// // Alternatively, you can use cli.DownloadMediaWithPath and provide the individual fields manually.
// }
// }
func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []byte) error {
ciphertext, iv, err := encryptMediaRetryReceipt(message.ID, mediaKey)
if err != nil {
@ -104,6 +133,7 @@ func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []
}
// DecryptMediaRetryNotification decrypts a media retry notification using the media key.
// See Client.SendMediaRetryReceipt for more info on how to use this.
func DecryptMediaRetryNotification(evt *events.MediaRetry, mediaKey []byte) (*waProto.MediaRetryNotification, error) {
var notif waProto.MediaRetryNotification
var plaintext []byte
@ -126,7 +156,7 @@ func DecryptMediaRetryNotification(evt *events.MediaRetry, mediaKey []byte) (*wa
func parseMediaRetryNotification(node *waBinary.Node) (*events.MediaRetry, error) {
ag := node.AttrGetter()
var evt events.MediaRetry
evt.Timestamp = time.Unix(ag.Int64("t"), 0)
evt.Timestamp = ag.UnixTime("t")
evt.MessageID = types.MessageID(ag.String("id"))
if !ag.OK() {
return nil, ag.Error()

View File

@ -10,11 +10,11 @@ import (
"bytes"
"compress/zlib"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"runtime/debug"
"strconv"
"sync/atomic"
"time"
@ -48,67 +48,50 @@ func (cli *Client) handleEncryptedMessage(node *waBinary.Node) {
}
func (cli *Client) parseMessageSource(node *waBinary.Node) (source types.MessageSource, err error) {
from, ok := node.Attrs["from"].(types.JID)
if !ok {
err = fmt.Errorf("didn't find valid `from` attribute in message")
} else if from.Server == types.GroupServer || from.Server == types.BroadcastServer {
ag := node.AttrGetter()
from := ag.JID("from")
if from.Server == types.GroupServer || from.Server == types.BroadcastServer {
source.IsGroup = true
source.Chat = from
sender, ok := node.Attrs["participant"].(types.JID)
if !ok {
err = fmt.Errorf("didn't find valid `participant` attribute in group message")
} else {
source.Sender = sender
if source.Sender.User == cli.Store.ID.User {
source.IsFromMe = true
}
source.Sender = ag.JID("participant")
if source.Sender.User == cli.Store.ID.User {
source.IsFromMe = true
}
if from.Server == types.BroadcastServer {
recipient, ok := node.Attrs["recipient"].(types.JID)
if ok {
source.BroadcastListOwner = recipient
}
source.BroadcastListOwner = ag.OptionalJIDOrEmpty("recipient")
}
} else if from.User == cli.Store.ID.User {
source.IsFromMe = true
source.Sender = from
recipient, ok := node.Attrs["recipient"].(types.JID)
if !ok {
source.Chat = from.ToNonAD()
recipient := ag.OptionalJID("recipient")
if recipient != nil {
source.Chat = *recipient
} else {
source.Chat = recipient
source.Chat = from.ToNonAD()
}
} else {
source.Chat = from.ToNonAD()
source.Sender = from
}
err = ag.Error()
return
}
func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, error) {
var info types.MessageInfo
var err error
var ok bool
info.MessageSource, err = cli.parseMessageSource(node)
if err != nil {
return nil, err
}
info.ID, ok = node.Attrs["id"].(string)
if !ok {
return nil, fmt.Errorf("didn't find valid `id` attribute in message")
ag := node.AttrGetter()
info.ID = types.MessageID(ag.String("id"))
info.Timestamp = ag.UnixTime("t")
info.PushName = ag.OptionalString("notify")
info.Category = ag.OptionalString("category")
if !ag.OK() {
return nil, ag.Error()
}
ts, ok := node.Attrs["t"].(string)
if !ok {
return nil, fmt.Errorf("didn't find valid `t` (timestamp) attribute in message")
}
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return nil, fmt.Errorf("didn't find valid `t` (timestamp) attribute in message: %w", err)
}
info.Timestamp = time.Unix(tsInt, 0)
info.PushName, _ = node.Attrs["notify"].(string)
info.Category, _ = node.Attrs["category"].(string)
for _, child := range node.GetChildren() {
if child.Tag == "multicast" {
@ -132,6 +115,7 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
children := node.GetChildren()
cli.Log.Debugf("Decrypting %d messages from %s", len(children), info.SourceString())
handled := false
containsDirectMsg := false
for _, child := range children {
if child.Tag != "enc" {
continue
@ -144,6 +128,7 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
var err error
if encType == "pkmsg" || encType == "msg" {
decrypted, err = cli.decryptDM(&child, info.Sender, encType == "pkmsg")
containsDirectMsg = true
} else if info.IsGroup && encType == "skmsg" {
decrypted, err = cli.decryptGroupMsg(&child, info.Sender, info.Chat)
} else {
@ -152,8 +137,9 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
}
if err != nil {
cli.Log.Warnf("Error decrypting message from %s: %v", info.SourceString(), err)
go cli.sendRetryReceipt(node, false)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: false})
isUnavailable := encType == "skmsg" && !containsDirectMsg && errors.Is(err, signalerror.ErrNoSenderKeyForUser)
go cli.sendRetryReceipt(node, isUnavailable)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: isUnavailable})
return
}
@ -317,27 +303,35 @@ func (cli *Client) handleHistorySyncNotification(notif *waProto.HistorySyncNotif
}
func (cli *Client) handleAppStateSyncKeyShare(keys *waProto.AppStateSyncKeyShare) {
onlyResyncIfNotSynced := true
cli.Log.Debugf("Got %d new app state keys", len(keys.GetKeys()))
cli.appStateKeyRequestsLock.RLock()
for _, key := range keys.GetKeys() {
marshaledFingerprint, err := proto.Marshal(key.GetKeyData().GetFingerprint())
if err != nil {
cli.Log.Errorf("Failed to marshal fingerprint of app state sync key %X", key.GetKeyId().GetKeyId())
continue
}
_, isReRequest := cli.appStateKeyRequests[hex.EncodeToString(key.GetKeyId().GetKeyId())]
if isReRequest {
onlyResyncIfNotSynced = false
}
err = cli.Store.AppStateKeys.PutAppStateSyncKey(key.GetKeyId().GetKeyId(), store.AppStateSyncKey{
Data: key.GetKeyData().GetKeyData(),
Fingerprint: marshaledFingerprint,
Timestamp: key.GetKeyData().GetTimestamp(),
})
if err != nil {
cli.Log.Errorf("Failed to store app state sync key %X", key.GetKeyId().GetKeyId())
cli.Log.Errorf("Failed to store app state sync key %X: %v", key.GetKeyId().GetKeyId(), err)
continue
}
cli.Log.Debugf("Received app state sync key %X", key.GetKeyId().GetKeyId())
}
cli.appStateKeyRequestsLock.RUnlock()
for _, name := range appstate.AllPatchNames {
err := cli.FetchAppState(name, false, true)
err := cli.FetchAppState(name, false, onlyResyncIfNotSynced)
if err != nil {
cli.Log.Errorf("Failed to do initial fetch of app state %s: %v", name, err)
}
@ -364,18 +358,11 @@ func (cli *Client) handleProtocolMessage(info *types.MessageInfo, msg *waProto.M
}
}
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message) {
evt := &events.Message{Info: *info, RawMessage: msg}
// First unwrap device sent messages
func (cli *Client) processProtocolParts(info *types.MessageInfo, msg *waProto.Message) {
// Hopefully sender key distribution messages and protocol messages can't be inside ephemeral messages
if msg.GetDeviceSentMessage().GetMessage() != nil {
msg = msg.GetDeviceSentMessage().GetMessage()
evt.Info.DeviceSentMeta = &types.DeviceSentMeta{
DestinationJID: msg.GetDeviceSentMessage().GetDestinationJid(),
Phash: msg.GetDeviceSentMessage().GetPhash(),
}
}
if msg.GetSenderKeyDistributionMessage() != nil {
if !info.IsGroup {
cli.Log.Warnf("Got sender key distribution message in non-group chat from", info.Sender)
@ -387,19 +374,12 @@ func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.
cli.handleProtocolMessage(info, msg)
}
// Unwrap ephemeral and view-once messages
// Hopefully sender key distribution messages and protocol messages can't be inside ephemeral messages
if msg.GetEphemeralMessage().GetMessage() != nil {
msg = msg.GetEphemeralMessage().GetMessage()
evt.IsEphemeral = true
}
if msg.GetViewOnceMessage().GetMessage() != nil {
msg = msg.GetViewOnceMessage().GetMessage()
evt.IsViewOnce = true
}
evt.Message = msg
}
cli.dispatchEvent(evt)
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message) {
cli.processProtocolParts(info, msg)
evt := &events.Message{Info: *info, RawMessage: msg}
cli.dispatchEvent(evt.UnwrapRaw())
}
func (cli *Client) sendProtocolMessageReceipt(id, msgType string) {

View File

@ -8,7 +8,6 @@ package whatsmeow
import (
"errors"
"time"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
@ -40,7 +39,7 @@ func (cli *Client) handleEncryptNotification(node *waBinary.Node) {
if err != nil {
cli.Log.Warnf("Failed to delete all sessions of %s from store after identity change: %v", from, err)
}
ts := time.Unix(node.AttrGetter().Int64("t"), 0)
ts := node.AttrGetter().UnixTime("t")
cli.dispatchEvent(&events.IdentityChange{JID: from, Timestamp: ts})
} else {
cli.Log.Debugf("Got unknown encryption notification from server: %s", node.XMLString())
@ -65,7 +64,7 @@ func (cli *Client) handleAppStateNotification(node *waBinary.Node) {
}
func (cli *Client) handlePictureNotification(node *waBinary.Node) {
ts := time.Unix(node.AttrGetter().Int64("t"), 0)
ts := node.AttrGetter().UnixTime("t")
for _, child := range node.GetChildren() {
ag := child.AttrGetter()
var evt events.Picture

View File

@ -138,13 +138,13 @@ func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, p
return fmt.Errorf("failed to parse device identity details in pair success message: %w", err)
}
cli.Store.Account = proto.Clone(&deviceIdentity).(*waProto.ADVSignedDeviceIdentity)
mainDeviceJID := jid
mainDeviceJID.Device = 0
mainDeviceIdentity := *(*[32]byte)(deviceIdentity.AccountSignatureKey)
deviceIdentity.AccountSignatureKey = nil
cli.Store.Account = proto.Clone(&deviceIdentity).(*waProto.ADVSignedDeviceIdentity)
selfSignedDeviceIdentity, err := proto.Marshal(&deviceIdentity)
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")

View File

@ -8,7 +8,6 @@ package whatsmeow
import (
"sync/atomic"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
@ -48,7 +47,7 @@ func (cli *Client) handlePresence(node *waBinary.Node) {
}
lastSeen := ag.OptionalString("last")
if lastSeen != "" && lastSeen != "deny" {
evt.LastSeen = time.Unix(ag.Int64("last"), 0)
evt.LastSeen = ag.UnixTime("last")
}
if !ag.OK() {
cli.Log.Warnf("Error parsing presence event: %+v", ag.Errors)

View File

@ -42,7 +42,7 @@ func (cli *Client) parseReceipt(node *waBinary.Node) (*events.Receipt, error) {
}
receipt := events.Receipt{
MessageSource: source,
Timestamp: time.Unix(ag.Int64("t"), 0),
Timestamp: ag.UnixTime("t"),
Type: events.ReceiptType(ag.OptionalString("type")),
}
mainMessageID := ag.String("id")

View File

@ -8,7 +8,7 @@ package whatsmeow
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"sync/atomic"
"time"
@ -27,6 +27,20 @@ func isDisconnectNode(node *waBinary.Node) bool {
return node == xmlStreamEndNode || node.Tag == "stream:error"
}
// isAuthErrorDisconnect checks if the given disconnect node is an error that shouldn't cause retrying.
func isAuthErrorDisconnect(node *waBinary.Node) bool {
if node.Tag != "stream:error" {
return false
}
code, _ := node.Attrs["code"].(string)
conflict, _ := node.GetOptionalChildByTag("conflict")
conflictType := conflict.AttrGetter().OptionalString("type")
if code == "401" || conflictType == "replaced" || conflictType == "device_removed" {
return true
}
return false
}
func (cli *Client) clearResponseWaiters(node *waBinary.Node) {
cli.responseWaitersLock.Lock()
for _, waiter := range cli.responseWaiters {
@ -88,10 +102,11 @@ type infoQuery struct {
Content interface{}
Timeout time.Duration
NoRetry bool
Context context.Context
}
func (cli *Client) sendIQAsyncDebug(query infoQuery) (<-chan *waBinary.Node, []byte, error) {
func (cli *Client) sendIQAsyncAndGetData(query *infoQuery) (<-chan *waBinary.Node, []byte, error) {
if len(query.ID) == 0 {
query.ID = cli.generateRequestID()
}
@ -107,7 +122,7 @@ func (cli *Client) sendIQAsyncDebug(query infoQuery) (<-chan *waBinary.Node, []b
if !query.Target.IsEmpty() {
attrs["target"] = query.Target
}
data, err := cli.sendNodeDebug(waBinary.Node{
data, err := cli.sendNodeAndGetData(waBinary.Node{
Tag: "iq",
Attrs: attrs,
Content: query.Content,
@ -120,12 +135,12 @@ func (cli *Client) sendIQAsyncDebug(query infoQuery) (<-chan *waBinary.Node, []b
}
func (cli *Client) sendIQAsync(query infoQuery) (<-chan *waBinary.Node, error) {
ch, _, err := cli.sendIQAsyncDebug(query)
ch, _, err := cli.sendIQAsyncAndGetData(&query)
return ch, err
}
func (cli *Client) sendIQ(query infoQuery) (*waBinary.Node, error) {
resChan, data, err := cli.sendIQAsyncDebug(query)
resChan, data, err := cli.sendIQAsyncAndGetData(&query)
if err != nil {
return nil, err
}
@ -138,10 +153,13 @@ func (cli *Client) sendIQ(query infoQuery) (*waBinary.Node, error) {
select {
case res := <-resChan:
if isDisconnectNode(res) {
if cli.DebugDecodeBeforeSend && res.Tag == "stream:error" && res.GetChildByTag("xml-not-well-formed").Tag != "" {
cli.Log.Debugf("Info query that was interrupted by xml-not-well-formed: %s", base64.URLEncoding.EncodeToString(data))
if query.NoRetry {
return nil, &DisconnectedError{Action: "info query", Node: res}
}
res, err = cli.retryFrame("info query", query.ID, data, res, query.Context, query.Timeout)
if err != nil {
return nil, err
}
return nil, &DisconnectedError{Action: "info query", Node: res}
}
resType, _ := res.Attrs["type"].(string)
if res.Tag != "iq" || (resType != "result" && resType != "error") {
@ -156,3 +174,48 @@ func (cli *Client) sendIQ(query infoQuery) (*waBinary.Node, error) {
return nil, ErrIQTimedOut
}
}
func (cli *Client) retryFrame(reqType, id string, data []byte, origResp *waBinary.Node, ctx context.Context, timeout time.Duration) (*waBinary.Node, error) {
if isAuthErrorDisconnect(origResp) {
cli.Log.Debugf("%s (%s) was interrupted by websocket disconnection (%s), not retrying as it looks like an auth error", id, reqType, origResp.XMLString())
return nil, &DisconnectedError{Action: reqType, Node: origResp}
}
cli.Log.Debugf("%s (%s) was interrupted by websocket disconnection (%s), waiting for reconnect to retry...", id, reqType, origResp.XMLString())
if !cli.WaitForConnection(5 * time.Second) {
cli.Log.Debugf("Websocket didn't reconnect within 5 seconds of failed %s (%s)", reqType, id)
return nil, &DisconnectedError{Action: reqType, Node: origResp}
}
cli.socketLock.RLock()
sock := cli.socket
cli.socketLock.RUnlock()
if sock == nil {
return nil, ErrNotConnected
}
respChan := cli.waitResponse(id)
err := sock.SendFrame(data)
if err != nil {
cli.cancelResponse(id, respChan)
return nil, err
}
var resp *waBinary.Node
if ctx != nil && timeout > 0 {
select {
case resp = <-respChan:
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(timeout):
// FIXME this error isn't technically correct (but works for now - the ctx and timeout params are only used from sendIQ)
return nil, ErrIQTimedOut
}
} else {
resp = <-respChan
}
if isDisconnectNode(resp) {
cli.Log.Debugf("Retrying %s %s was interrupted by websocket disconnection (%v), not retrying anymore", reqType, id, resp.XMLString())
return nil, &DisconnectedError{Action: fmt.Sprintf("%s (retry)", reqType), Node: resp}
}
return resp, nil
}

View File

@ -62,7 +62,7 @@ func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) *waProto.M
func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.MessageID) (*waProto.Message, error) {
msg := cli.getRecentMessage(receipt.Chat, messageID)
if msg == nil {
msg = cli.GetMessageForRetry(receipt.Chat, messageID)
msg = cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID)
if msg == nil {
return nil, fmt.Errorf("couldn't find message %s", messageID)
} else {
@ -74,6 +74,25 @@ func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.M
return proto.Clone(msg).(*waProto.Message), nil
}
const recreateSessionTimeout = 1 * time.Hour
func (cli *Client) shouldRecreateSession(retryCount int, jid types.JID) (reason string, recreate bool) {
cli.sessionRecreateHistoryLock.Lock()
defer cli.sessionRecreateHistoryLock.Unlock()
if !cli.Store.ContainsSession(jid.SignalAddress()) {
cli.sessionRecreateHistory[jid] = time.Now()
return "we don't have a Signal session with them", true
} else if retryCount < 2 {
return "", false
}
prevTime, ok := cli.sessionRecreateHistory[jid]
if !ok || prevTime.Add(recreateSessionTimeout).Before(time.Now()) {
cli.sessionRecreateHistory[jid] = time.Now()
return "retry count > 1 and over an hour since last recreation", true
}
return "", false
}
// handleRetryReceipt handles an incoming retry receipt for an outgoing message.
func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.Node) error {
retryChild, ok := node.GetOptionalChildByTag("retry")
@ -82,7 +101,7 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
}
ag := retryChild.AttrGetter()
messageID := ag.String("id")
timestamp := time.Unix(ag.Int64("t"), 0)
timestamp := ag.UnixTime("t")
retryCount := ag.Int("count")
if !ag.OK() {
return ag.Error()
@ -113,7 +132,7 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
}
}
if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, retryCount, msg) {
if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, messageID, retryCount, msg) {
cli.Log.Debugf("Cancelled retry receipt in PreRetryCallback")
return nil
}
@ -129,12 +148,8 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if err != nil {
return fmt.Errorf("failed to read prekey bundle in retry receipt: %w", err)
}
} else if retryCount >= 2 || !cli.Store.ContainsSession(receipt.Sender.SignalAddress()) {
if retryCount >= 2 {
cli.Log.Debugf("Fetching prekeys for %s due to retry receipt with count>1 but no prekey bundle", receipt.Sender)
} else {
cli.Log.Debugf("Fetching prekeys for %s for handling retry receipt because we don't have a Signal session with them", receipt.Sender)
}
} else if reason, recreate := cli.shouldRecreateSession(retryCount, receipt.Sender); recreate {
cli.Log.Debugf("Fetching prekeys for %s for handling retry receipt with no prekey bundle because %s", receipt.Sender, reason)
var keys map[types.JID]preKeyResp
keys, err = cli.fetchPreKeys([]types.JID{receipt.Sender})
if err != nil {
@ -148,13 +163,6 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
} else if bundle == nil {
return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", senderAD, len(keys))
}
if retryCount > 3 {
cli.Log.Debugf("Erasing existing session for %s due to retry receipt with count>3", receipt.Sender)
err = cli.Store.Sessions.DeleteSession(receipt.Sender.SignalAddress().String())
if err != nil {
return fmt.Errorf("failed to delete session for %s: %w", senderAD, err)
}
}
}
encrypted, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle)
if err != nil {
@ -164,7 +172,7 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
attrs := waBinary.Attrs{
"to": node.Attrs["from"],
"type": "text",
"type": getTypeFromMessage(msg),
"id": messageID,
"t": timestamp.Unix(),
}
@ -180,18 +188,15 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if edit, ok := node.Attrs["edit"]; ok {
attrs["edit"] = edit
}
req := waBinary.Node{
content := []waBinary.Node{*encrypted}
if includeDeviceIdentity {
content = append(content, cli.makeDeviceIdentityNode())
}
err = cli.sendNode(waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: []waBinary.Node{*encrypted},
}
if includeDeviceIdentity {
err = cli.appendDeviceIdentityNode(&req)
if err != nil {
return fmt.Errorf("failed to add device identity to retry message: %w", err)
}
}
err = cli.sendNode(req)
Content: content,
})
if err != nil {
return fmt.Errorf("failed to send retry message: %w", err)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Tulir Asokan
// Copyright (c) 2022 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
@ -14,6 +14,7 @@ import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
@ -65,7 +66,8 @@ func GenerateMessageID() types.MessageID {
// For other message types, you'll have to figure it out yourself. Looking at the protobuf schema
// in binary/proto/def.proto may be useful to find out all the allowed fields.
func (cli *Client) SendMessage(to types.JID, id types.MessageID, message *waProto.Message) (time.Time, error) {
if to.AD {
isPeerMessage := to.User == cli.Store.ID.User
if to.AD && !isPeerMessage {
return time.Time{}, ErrRecipientADJID
}
@ -73,23 +75,27 @@ func (cli *Client) SendMessage(to types.JID, id types.MessageID, message *waProt
id = GenerateMessageID()
}
if cli.OneMessageAtATime {
cli.messageSendLock.Lock()
defer cli.messageSendLock.Unlock()
}
// Sending multiple messages at a time can cause weird issues and makes it harder to retry safely
cli.messageSendLock.Lock()
defer cli.messageSendLock.Unlock()
cli.addRecentMessage(to, id, message)
respChan := cli.waitResponse(id)
// Peer message retries aren't implemented yet
if !isPeerMessage {
cli.addRecentMessage(to, id, message)
}
var err error
var phash string
var data []byte
switch to.Server {
case types.GroupServer:
case types.GroupServer, types.BroadcastServer:
phash, data, err = cli.sendGroup(to, id, message)
case types.DefaultUserServer:
data, err = cli.sendDM(to, id, message)
case types.BroadcastServer:
err = ErrBroadcastListUnsupported
if isPeerMessage {
data, err = cli.sendPeerMessage(to, id, message)
} else {
data, err = cli.sendDM(to, id, message)
}
default:
err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server)
}
@ -99,13 +105,13 @@ func (cli *Client) SendMessage(to types.JID, id types.MessageID, message *waProt
}
resp := <-respChan
if isDisconnectNode(resp) {
if cli.DebugDecodeBeforeSend && resp.Tag == "stream:error" && resp.GetChildByTag("xml-not-well-formed").Tag != "" {
cli.Log.Debugf("Message that was interrupted by xml-not-well-formed: %s", base64.URLEncoding.EncodeToString(data))
resp, err = cli.retryFrame("message send", id, data, resp, nil, 0)
if err != nil {
return time.Time{}, err
}
return time.Time{}, &DisconnectedError{Action: "message send", Node: resp}
}
ag := resp.AttrGetter()
ts := time.Unix(ag.Int64("t"), 0)
ts := ag.UnixTime("t")
expectedPHash := ag.OptionalString("phash")
if len(expectedPHash) > 0 && phash != expectedPHash {
cli.Log.Warnf("Server returned different participant list hash when sending to %s. Some devices may not have received the message.", to)
@ -135,6 +141,66 @@ func (cli *Client) RevokeMessage(chat types.JID, id types.MessageID) (time.Time,
})
}
const (
DisappearingTimerOff = time.Duration(0)
DisappearingTimer24Hours = 24 * time.Hour
DisappearingTimer7Days = 7 * 24 * time.Hour
DisappearingTimer90Days = 90 * 24 * time.Hour
)
// ParseDisappearingTimerString parses common human-readable disappearing message timer strings into Duration values.
// If the string doesn't look like one of the allowed values (0, 24h, 7d, 90d), the second return value is false.
func ParseDisappearingTimerString(val string) (time.Duration, bool) {
switch strings.ReplaceAll(strings.ToLower(val), " ", "") {
case "0d", "0h", "0s", "0", "off":
return DisappearingTimerOff, true
case "1day", "day", "1d", "1", "24h", "24", "86400s", "86400":
return DisappearingTimer24Hours, true
case "1week", "week", "7d", "7", "168h", "168", "604800s", "604800":
return DisappearingTimer7Days, true
case "3months", "3m", "3mo", "90d", "90", "2160h", "2160", "7776000s", "7776000":
return DisappearingTimer90Days, true
default:
return 0, false
}
}
// SetDisappearingTimer sets the disappearing timer in a chat. Both private chats and groups are supported, but they're
// set with different methods.
//
// Note that while this function allows passing non-standard durations, official WhatsApp apps will ignore those,
// and in groups the server will just reject the change. You can use the DisappearingTimer<Duration> constants for convenience.
//
// In groups, the server will echo the change as a notification, so it'll show up as a *events.GroupInfo update.
func (cli *Client) SetDisappearingTimer(chat types.JID, timer time.Duration) (err error) {
switch chat.Server {
case types.DefaultUserServer:
_, err = cli.SendMessage(chat, "", &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_EPHEMERAL_SETTING.Enum(),
EphemeralExpiration: proto.Uint32(uint32(timer.Seconds())),
},
})
case types.GroupServer:
if timer == 0 {
_, err = cli.sendGroupIQ(iqSet, chat, waBinary.Node{Tag: "not_ephemeral"})
} else {
_, err = cli.sendGroupIQ(iqSet, chat, waBinary.Node{
Tag: "ephemeral",
Attrs: waBinary.Attrs{
"expiration": strconv.Itoa(int(timer.Seconds())),
},
})
if errors.Is(err, ErrIQBadRequest) {
err = wrapIQError(ErrInvalidDisappearingTimer, err)
}
}
default:
err = fmt.Errorf("can't set disappearing time in a %s chat", chat.Server)
}
return
}
func participantListHashV2(participants []types.JID) string {
participantsStrings := make([]string, len(participants))
for i, part := range participants {
@ -147,9 +213,18 @@ func participantListHashV2(participants []types.JID) string {
}
func (cli *Client) sendGroup(to types.JID, id types.MessageID, message *waProto.Message) (string, []byte, error) {
participants, err := cli.getGroupMembers(to)
if err != nil {
return "", nil, fmt.Errorf("failed to get group members: %w", err)
var participants []types.JID
var err error
if to.Server == types.GroupServer {
participants, err = cli.getGroupMembers(to)
if err != nil {
return "", nil, fmt.Errorf("failed to get group members: %w", err)
}
} else {
participants, err = cli.getBroadcastListParticipants(to)
if err != nil {
return "", nil, fmt.Errorf("failed to get broadcast list members: %w", err)
}
}
plaintext, _, err := marshalMessage(to, message)
@ -194,13 +269,25 @@ func (cli *Client) sendGroup(to types.JID, id types.MessageID, message *waProto.
Attrs: waBinary.Attrs{"v": "2", "type": "skmsg"},
})
data, err := cli.sendNodeDebug(*node)
data, err := cli.sendNodeAndGetData(*node)
if err != nil {
return "", nil, fmt.Errorf("failed to send message node: %w", err)
}
return phash, data, nil
}
func (cli *Client) sendPeerMessage(to types.JID, id types.MessageID, message *waProto.Message) ([]byte, error) {
node, err := cli.preparePeerMessageNode(to, id, message)
if err != nil {
return nil, err
}
data, err := cli.sendNodeAndGetData(*node)
if err != nil {
return nil, fmt.Errorf("failed to send message node: %w", err)
}
return data, nil
}
func (cli *Client) sendDM(to types.JID, id types.MessageID, message *waProto.Message) ([]byte, error) {
messagePlaintext, deviceSentMessagePlaintext, err := marshalMessage(to, message)
if err != nil {
@ -211,46 +298,102 @@ func (cli *Client) sendDM(to types.JID, id types.MessageID, message *waProto.Mes
if err != nil {
return nil, err
}
data, err := cli.sendNodeDebug(*node)
data, err := cli.sendNodeAndGetData(*node)
if err != nil {
return nil, fmt.Errorf("failed to send message node: %w", err)
}
return data, nil
}
func getTypeFromMessage(msg *waProto.Message) string {
switch {
case msg.ViewOnceMessage != nil:
return getTypeFromMessage(msg.ViewOnceMessage.Message)
case msg.EphemeralMessage != nil:
return getTypeFromMessage(msg.EphemeralMessage.Message)
case msg.ReactionMessage != nil:
return "reaction"
case msg.Conversation != nil, msg.ExtendedTextMessage != nil, msg.ProtocolMessage != nil:
return "text"
//TODO this requires setting mediatype in the enc nodes
//case msg.ImageMessage != nil, msg.DocumentMessage != nil, msg.AudioMessage != nil, msg.VideoMessage != nil:
// return "media"
default:
return "text"
}
}
func getEditAttribute(msg *waProto.Message) string {
if msg.ProtocolMessage != nil && msg.GetProtocolMessage().GetType() == waProto.ProtocolMessage_REVOKE && msg.GetProtocolMessage().GetKey() != nil {
if msg.GetProtocolMessage().GetKey().GetFromMe() {
return "7"
} else {
return "8"
}
} else if msg.ReactionMessage != nil && msg.ReactionMessage.GetText() == "" {
return "7"
}
return ""
}
func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, message *waProto.Message) (*waBinary.Node, error) {
attrs := waBinary.Attrs{
"id": id,
"type": "text",
"category": "peer",
"to": to,
}
if message.GetProtocolMessage().GetType() == waProto.ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST {
attrs["push_priority"] = "high"
}
plaintext, err := proto.Marshal(message)
if err != nil {
err = fmt.Errorf("failed to marshal message: %w", err)
return nil, err
}
encrypted, isPreKey, err := cli.encryptMessageForDevice(plaintext, to, nil)
if err != nil {
return nil, fmt.Errorf("failed to encrypt peer message for %s: %v", to, err)
}
content := []waBinary.Node{*encrypted}
if isPreKey {
content = append(content, cli.makeDeviceIdentityNode())
}
return &waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: content,
}, nil
}
func (cli *Client) prepareMessageNode(to types.JID, id types.MessageID, message *waProto.Message, participants []types.JID, plaintext, dsmPlaintext []byte) (*waBinary.Node, []types.JID, error) {
allDevices, err := cli.GetUserDevices(participants)
if err != nil {
return nil, nil, fmt.Errorf("failed to get device list: %w", err)
}
participantNodes, includeIdentity := cli.encryptMessageForDevices(allDevices, id, plaintext, dsmPlaintext)
node := waBinary.Node{
Tag: "message",
Attrs: waBinary.Attrs{
"id": id,
"type": "text",
"to": to,
},
Content: []waBinary.Node{{
Tag: "participants",
Content: participantNodes,
}},
attrs := waBinary.Attrs{
"id": id,
"type": getTypeFromMessage(message),
"to": to,
}
if message.ProtocolMessage != nil && message.GetProtocolMessage().GetType() == waProto.ProtocolMessage_REVOKE && message.GetProtocolMessage().GetKey() != nil {
if message.GetProtocolMessage().GetKey().GetFromMe() {
node.Attrs["edit"] = "7"
} else {
node.Attrs["edit"] = "8"
}
if editAttr := getEditAttribute(message); editAttr != "" {
attrs["edit"] = editAttr
}
participantNodes, includeIdentity := cli.encryptMessageForDevices(allDevices, id, plaintext, dsmPlaintext)
content := []waBinary.Node{{
Tag: "participants",
Content: participantNodes,
}}
if includeIdentity {
err := cli.appendDeviceIdentityNode(&node)
if err != nil {
return nil, nil, err
}
content = append(content, cli.makeDeviceIdentityNode())
}
return &node, allDevices, nil
return &waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: content,
}, allDevices, nil
}
func marshalMessage(to types.JID, message *waProto.Message) (plaintext, dsmPlaintext []byte, err error) {
@ -276,16 +419,15 @@ func marshalMessage(to types.JID, message *waProto.Message) (plaintext, dsmPlain
return
}
func (cli *Client) appendDeviceIdentityNode(node *waBinary.Node) error {
func (cli *Client) makeDeviceIdentityNode() waBinary.Node {
deviceIdentity, err := proto.Marshal(cli.Store.Account)
if err != nil {
return fmt.Errorf("failed to marshal device identity: %w", err)
panic(fmt.Errorf("failed to marshal device identity: %w", err))
}
node.Content = append(node.GetChildren(), waBinary.Node{
return waBinary.Node{
Tag: "device-identity",
Content: deviceIdentity,
})
return nil
}
}
func (cli *Client) encryptMessageForDevices(allDevices []types.JID, id string, msgPlaintext, dsmPlaintext []byte) ([]waBinary.Node, bool) {

View File

@ -74,7 +74,7 @@ func (vc WAVersionContainer) ProtoAppVersion() *waProto.AppVersion {
}
// waVersion is the WhatsApp web client version
var waVersion = WAVersionContainer{2, 2214, 12}
var waVersion = WAVersionContainer{2, 2218, 8}
// waVersionHash is the md5 hash of a dot-separated waVersion
var waVersionHash [16]byte
@ -122,7 +122,10 @@ var BaseClientPayload = &waProto.ClientPayload{
ConnectReason: waProto.ClientPayload_USER_ACTIVATED.Enum(),
}
var CompanionProps = &waProto.CompanionProps{
// Deprecated: renamed to DeviceProps
var CompanionProps = DeviceProps
var DeviceProps = &waProto.CompanionProps{
Os: proto.String("whatsmeow"),
Version: &waProto.AppVersion{
Primary: proto.Uint32(0),
@ -134,10 +137,10 @@ var CompanionProps = &waProto.CompanionProps{
}
func SetOSInfo(name string, version [3]uint32) {
CompanionProps.Os = &name
CompanionProps.Version.Primary = &version[0]
CompanionProps.Version.Secondary = &version[1]
CompanionProps.Version.Tertiary = &version[2]
DeviceProps.Os = &name
DeviceProps.Version.Primary = &version[0]
DeviceProps.Version.Secondary = &version[1]
DeviceProps.Version.Tertiary = &version[2]
BaseClientPayload.UserAgent.OsVersion = proto.String(fmt.Sprintf("%d.%d.%d", version[0], version[1], version[2]))
BaseClientPayload.UserAgent.OsBuildNumber = BaseClientPayload.UserAgent.OsVersion
}
@ -148,16 +151,16 @@ func (device *Device) getRegistrationPayload() *waProto.ClientPayload {
binary.BigEndian.PutUint32(regID, device.RegistrationID)
preKeyID := make([]byte, 4)
binary.BigEndian.PutUint32(preKeyID, device.SignedPreKey.KeyID)
companionProps, _ := proto.Marshal(CompanionProps)
payload.RegData = &waProto.CompanionRegData{
ERegid: regID,
EKeytype: []byte{ecc.DjbType},
EIdent: device.IdentityKey.Pub[:],
ESkeyId: preKeyID[1:],
ESkeyVal: device.SignedPreKey.Pub[:],
ESkeySig: device.SignedPreKey.Signature[:],
BuildHash: waVersionHash[:],
CompanionProps: companionProps,
deviceProps, _ := proto.Marshal(DeviceProps)
payload.DevicePairingData = &waProto.DevicePairingRegistrationData{
ERegid: regID,
EKeytype: []byte{ecc.DjbType},
EIdent: device.IdentityKey.Pub[:],
ESkeyId: preKeyID[1:],
ESkeyVal: device.SignedPreKey.Pub[:],
ESkeySig: device.SignedPreKey.Signature[:],
BuildHash: waVersionHash[:],
DeviceProps: deviceProps,
}
payload.Passive = proto.Bool(false)
return payload

View File

@ -78,7 +78,7 @@ func NewWithDB(db *sql.DB, dialect string, log waLog.Logger) *Container {
const getAllDevicesQuery = `
SELECT jid, registration_id, noise_key, identity_key,
signed_pre_key, signed_pre_key_id, signed_pre_key_sig,
adv_key, adv_details, adv_account_sig, adv_device_sig,
adv_key, adv_details, adv_account_sig, adv_account_sig_key, adv_device_sig,
platform, business_name, push_name
FROM whatsmeow_device
`
@ -100,7 +100,7 @@ func (c *Container) scanDevice(row scannable) (*store.Device, error) {
err := row.Scan(
&device.ID, &device.RegistrationID, &noisePriv, &identityPriv,
&preKeyPriv, &device.SignedPreKey.KeyID, &preKeySig,
&device.AdvSecretKey, &account.Details, &account.AccountSignature, &account.DeviceSignature,
&device.AdvSecretKey, &account.Details, &account.AccountSignature, &account.AccountSignatureKey, &account.DeviceSignature,
&device.Platform, &device.BusinessName, &device.PushName)
if err != nil {
return nil, fmt.Errorf("failed to scan session: %w", err)
@ -178,9 +178,9 @@ const (
insertDeviceQuery = `
INSERT INTO whatsmeow_device (jid, registration_id, noise_key, identity_key,
signed_pre_key, signed_pre_key_id, signed_pre_key_sig,
adv_key, adv_details, adv_account_sig, adv_device_sig,
adv_key, adv_details, adv_account_sig, adv_account_sig_key, adv_device_sig,
platform, business_name, push_name)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
ON CONFLICT (jid) DO UPDATE SET platform=$12, business_name=$13, push_name=$14
`
deleteDeviceQuery = `DELETE FROM whatsmeow_device WHERE jid=$1`
@ -222,7 +222,7 @@ func (c *Container) PutDevice(device *store.Device) error {
_, err := c.db.Exec(insertDeviceQuery,
device.ID.String(), device.RegistrationID, device.NoiseKey.Priv[:], device.IdentityKey.Priv[:],
device.SignedPreKey.Priv[:], device.SignedPreKey.KeyID, device.SignedPreKey.Signature[:],
device.AdvSecretKey, device.Account.Details, device.Account.AccountSignature, device.Account.DeviceSignature,
device.AdvSecretKey, device.Account.Details, device.Account.AccountSignature, device.Account.AccountSignatureKey, device.Account.DeviceSignature,
device.Platform, device.BusinessName, device.PushName)
if !device.Initialized {

View File

@ -16,7 +16,7 @@ type upgradeFunc func(*sql.Tx, *Container) error
//
// This may be of use if you want to manage the database fully manually, but in most cases you
// should just call Container.Upgrade to let the library handle everything.
var Upgrades = [...]upgradeFunc{upgradeV1}
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2}
func (c *Container) getVersion() (int, error) {
_, err := c.db.Exec("CREATE TABLE IF NOT EXISTS whatsmeow_version (version INTEGER)")
@ -56,6 +56,7 @@ func (c *Container) Upgrade() error {
}
migrateFunc := Upgrades[version]
c.log.Infof("Upgrading database to v%d", version+1)
err = migrateFunc(tx, c)
if err != nil {
_ = tx.Rollback()
@ -212,3 +213,36 @@ func upgradeV1(tx *sql.Tx, _ *Container) error {
}
return nil
}
const fillSigKeyPostgres = `
UPDATE whatsmeow_device SET adv_account_sig_key=(
SELECT identity
FROM whatsmeow_identity_keys
WHERE our_jid=whatsmeow_device.jid
AND their_id=concat(split_part(whatsmeow_device.jid, '.', 1), ':0')
);
DELETE FROM whatsmeow_device WHERE adv_account_sig_key IS NULL;
ALTER TABLE whatsmeow_device ALTER COLUMN adv_account_sig_key SET NOT NULL;
`
const fillSigKeySQLite = `
UPDATE whatsmeow_device SET adv_account_sig_key=(
SELECT identity
FROM whatsmeow_identity_keys
WHERE our_jid=whatsmeow_device.jid
AND their_id=substr(whatsmeow_device.jid, 0, instr(whatsmeow_device.jid, '.')) || ':0'
)
`
func upgradeV2(tx *sql.Tx, container *Container) error {
_, err := tx.Exec("ALTER TABLE whatsmeow_device ADD COLUMN adv_account_sig_key bytea CHECK ( length(adv_account_sig_key) = 32 )")
if err != nil {
return err
}
if container.dialect == "postgres" {
_, err = tx.Exec(fillSigKeyPostgres)
} else {
_, err = tx.Exec(fillSigKeySQLite)
}
return err
}

View File

@ -55,9 +55,25 @@ type QRScannedWithoutMultidevice struct{}
// 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{}
// 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.
@ -205,6 +221,27 @@ type Message struct {
RawMessage *waProto.Message
}
// 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.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
}
return evt
}
// ReceiptType represents the type of a Receipt event.
type ReceiptType string

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 Tulir Asokan
// Copyright (c) 2022 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
@ -94,3 +94,23 @@ type PrivacySettings struct {
Profile PrivacySetting
ReadReceipts PrivacySetting
}
// StatusPrivacyType is the type of list in StatusPrivacy.
type StatusPrivacyType string
const (
// StatusPrivacyTypeContacts means statuses are sent to all contacts.
StatusPrivacyTypeContacts StatusPrivacyType = "contacts"
// StatusPrivacyTypeBlacklist means statuses are sent to all contacts, except the ones on the list.
StatusPrivacyTypeBlacklist StatusPrivacyType = "blacklist"
// StatusPrivacyTypeWhitelist means statuses are only sent to users on the list.
StatusPrivacyTypeWhitelist StatusPrivacyType = "whitelist"
)
// StatusPrivacy contains the settings for who to send status messages to by default.
type StatusPrivacy struct {
Type StatusPrivacyType
List []JID
IsDefault bool
}

View File

@ -123,22 +123,19 @@ func (cli *Client) GetUserInfo(jids []types.JID) (map[types.JID]types.UserInfo,
if child.Tag != "user" || !jidOK {
continue
}
var info types.UserInfo
verifiedName, err := parseVerifiedName(child.GetChildByTag("business"))
if err != nil {
cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
}
status, _ := child.GetChildByTag("status").Content.([]byte)
pictureID, _ := child.GetChildByTag("picture").Attrs["id"].(string)
devices := parseDeviceList(jid.User, child.GetChildByTag("devices"))
respData[jid] = types.UserInfo{
VerifiedName: verifiedName,
Status: string(status),
PictureID: pictureID,
Devices: devices,
}
info.Status = string(status)
info.PictureID, _ = child.GetChildByTag("picture").Attrs["id"].(string)
info.Devices = parseDeviceList(jid.User, child.GetChildByTag("devices"))
if verifiedName != nil {
cli.updateBusinessName(jid, verifiedName.Details.GetVerifiedName())
}
respData[jid] = info
}
return respData, nil
}