diff options
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow')
31 files changed, 1021 insertions, 342 deletions
diff --git a/vendor/go.mau.fi/whatsmeow/README.md b/vendor/go.mau.fi/whatsmeow/README.md index bcd65fa2..123239b5 100644 --- a/vendor/go.mau.fi/whatsmeow/README.md +++ b/vendor/go.mau.fi/whatsmeow/README.md @@ -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 diff --git a/vendor/go.mau.fi/whatsmeow/appstate.go b/vendor/go.mau.fi/whatsmeow/appstate.go index b9b99f26..ad97bdc2 100644 --- a/vendor/go.mau.fi/whatsmeow/appstate.go +++ b/vendor/go.mau.fi/whatsmeow/appstate.go @@ -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) + } +} diff --git a/vendor/go.mau.fi/whatsmeow/appstate/keys.go b/vendor/go.mau.fi/whatsmeow/appstate/keys.go index 30ecbcc4..ec19dc26 100644 --- a/vendor/go.mau.fi/whatsmeow/appstate/keys.go +++ b/vendor/go.mau.fi/whatsmeow/appstate/keys.go @@ -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 +} diff --git a/vendor/go.mau.fi/whatsmeow/binary/attrs.go b/vendor/go.mau.fi/whatsmeow/binary/attrs.go index 35acd396..d7d43f0a 100644 --- a/vendor/go.mau.fi/whatsmeow/binary/attrs.go +++ b/vendor/go.mau.fi/whatsmeow/binary/attrs.go @@ -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 diff --git a/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go b/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go index b32ecc60..5b91ccbc 100644 --- a/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go +++ b/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go @@ -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,17 +18252,23 @@ func (x *UserAgent) GetDeviceBoard() string { return "" } -type DNSSource struct { +type DevicePairingRegistrationData struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - DnsMethod *DNSSource_DNSSourceDNSResolutionMethod `protobuf:"varint,15,opt,name=dnsMethod,enum=proto.DNSSource_DNSSourceDNSResolutionMethod" json:"dnsMethod,omitempty"` - AppCached *bool `protobuf:"varint,16,opt,name=appCached" json:"appCached,omitempty"` + 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 *DNSSource) Reset() { - *x = DNSSource{} +func (x *DevicePairingRegistrationData) Reset() { + *x = DevicePairingRegistrationData{} if protoimpl.UnsafeEnabled { mi := &file_binary_proto_def_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -18254,13 +18276,13 @@ func (x *DNSSource) Reset() { } } -func (x *DNSSource) String() string { +func (x *DevicePairingRegistrationData) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DNSSource) ProtoMessage() {} +func (*DevicePairingRegistrationData) ProtoMessage() {} -func (x *DNSSource) ProtoReflect() protoreflect.Message { +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)) @@ -18272,128 +18294,122 @@ func (x *DNSSource) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DNSSource.ProtoReflect.Descriptor instead. -func (*DNSSource) Descriptor() ([]byte, []int) { +// Deprecated: Use DevicePairingRegistrationData.ProtoReflect.Descriptor instead. +func (*DevicePairingRegistrationData) Descriptor() ([]byte, []int) { return file_binary_proto_def_proto_rawDescGZIP(), []int{182} } -func (x *DNSSource) GetDnsMethod() DNSSource_DNSSourceDNSResolutionMethod { - if x != nil && x.DnsMethod != nil { - return *x.DnsMethod - } - return DNSSource_SYSTEM -} - -func (x *DNSSource) GetAppCached() bool { - if x != nil && x.AppCached != nil { - return *x.AppCached - } - 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 { +func (x *DevicePairingRegistrationData) GetERegid() []byte { if x != nil { return x.ERegid } return nil } -func (x *CompanionRegData) GetEKeytype() []byte { +func (x *DevicePairingRegistrationData) GetEKeytype() []byte { if x != nil { return x.EKeytype } return nil } -func (x *CompanionRegData) GetEIdent() []byte { +func (x *DevicePairingRegistrationData) GetEIdent() []byte { if x != nil { return x.EIdent } return nil } -func (x *CompanionRegData) GetESkeyId() []byte { +func (x *DevicePairingRegistrationData) GetESkeyId() []byte { if x != nil { return x.ESkeyId } return nil } -func (x *CompanionRegData) GetESkeyVal() []byte { +func (x *DevicePairingRegistrationData) GetESkeyVal() []byte { if x != nil { return x.ESkeyVal } return nil } -func (x *CompanionRegData) GetESkeySig() []byte { +func (x *DevicePairingRegistrationData) GetESkeySig() []byte { if x != nil { return x.ESkeySig } return nil } -func (x *CompanionRegData) GetBuildHash() []byte { +func (x *DevicePairingRegistrationData) GetBuildHash() []byte { if x != nil { return x.BuildHash } return nil } -func (x *CompanionRegData) GetCompanionProps() []byte { +func (x *DevicePairingRegistrationData) GetDeviceProps() []byte { if x != nil { - return x.CompanionProps + return x.DeviceProps } return nil } +type DNSSource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DnsMethod *DNSSource_DNSSourceDNSResolutionMethod `protobuf:"varint,15,opt,name=dnsMethod,enum=proto.DNSSource_DNSSourceDNSResolutionMethod" json:"dnsMethod,omitempty"` + AppCached *bool `protobuf:"varint,16,opt,name=appCached" json:"appCached,omitempty"` +} + +func (x *DNSSource) Reset() { + *x = DNSSource{} + if protoimpl.UnsafeEnabled { + mi := &file_binary_proto_def_proto_msgTypes[183] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DNSSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DNSSource) ProtoMessage() {} + +func (x *DNSSource) 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 DNSSource.ProtoReflect.Descriptor instead. +func (*DNSSource) Descriptor() ([]byte, []int) { + return file_binary_proto_def_proto_rawDescGZIP(), []int{183} +} + +func (x *DNSSource) GetDnsMethod() DNSSource_DNSSourceDNSResolutionMethod { + if x != nil && x.DnsMethod != nil { + return *x.DnsMethod + } + return DNSSource_SYSTEM +} + +func (x *DNSSource) GetAppCached() bool { + if x != nil && x.AppCached != nil { + return *x.AppCached + } + return false +} + 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: diff --git a/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw b/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw Binary files differindex d01d964f..9fb85cdd 100644 --- a/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw +++ b/vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw diff --git a/vendor/go.mau.fi/whatsmeow/binary/proto/def.proto b/vendor/go.mau.fi/whatsmeow/binary/proto/def.proto index fd15a61e..93531585 100644 --- a/vendor/go.mau.fi/whatsmeow/binary/proto/def.proto +++ b/vendor/go.mau.fi/whatsmeow/binary/proto/def.proto @@ -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; diff --git a/vendor/go.mau.fi/whatsmeow/broadcast.go b/vendor/go.mau.fi/whatsmeow/broadcast.go new file mode 100644 index 00000000..c1260a9c --- /dev/null +++ b/vendor/go.mau.fi/whatsmeow/broadcast.go @@ -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 +} diff --git a/vendor/go.mau.fi/whatsmeow/call.go b/vendor/go.mau.fi/whatsmeow/call.go index e48e791c..f4de9bd8 100644 --- a/vendor/go.mau.fi/whatsmeow/call.go +++ b/vendor/go.mau.fi/whatsmeow/call.go @@ -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"), } diff --git a/vendor/go.mau.fi/whatsmeow/client.go b/vendor/go.mau.fi/whatsmeow/client.go index 70c82130..ea3a297c 100644 --- a/vendor/go.mau.fi/whatsmeow/client.go +++ b/vendor/go.mau.fi/whatsmeow/client.go @@ -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 +} diff --git a/vendor/go.mau.fi/whatsmeow/connectionevents.go b/vendor/go.mau.fi/whatsmeow/connectionevents.go index 2c8d27c0..54fafccf 100644 --- a/vendor/go.mau.fi/whatsmeow/connectionevents.go +++ b/vendor/go.mau.fi/whatsmeow/connectionevents.go @@ -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() }() } diff --git a/vendor/go.mau.fi/whatsmeow/download.go b/vendor/go.mau.fi/whatsmeow/download.go index 6779478f..e9b8d4f1 100644 --- a/vendor/go.mau.fi/whatsmeow/download.go +++ b/vendor/go.mau.fi/whatsmeow/download.go @@ -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 } } diff --git a/vendor/go.mau.fi/whatsmeow/errors.go b/vendor/go.mau.fi/whatsmeow/errors.go index d11cbb37..5cdbd1d0 100644 --- a/vendor/go.mau.fi/whatsmeow/errors.go +++ b/vendor/go.mau.fi/whatsmeow/errors.go @@ -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"} diff --git a/vendor/go.mau.fi/whatsmeow/group.go b/vendor/go.mau.fi/whatsmeow/group.go index 34e3603f..e19a90a3 100644 --- a/vendor/go.mau.fi/whatsmeow/group.go +++ b/vendor/go.mau.fi/whatsmeow/group.go @@ -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": diff --git a/vendor/go.mau.fi/whatsmeow/internals.go b/vendor/go.mau.fi/whatsmeow/internals.go index 64a0b354..7a8d50b5 100644 --- a/vendor/go.mau.fi/whatsmeow/internals.go +++ b/vendor/go.mau.fi/whatsmeow/internals.go @@ -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) +} diff --git a/vendor/go.mau.fi/whatsmeow/keepalive.go b/vendor/go.mau.fi/whatsmeow/keepalive.go index ec05df1d..d5e40286 100644 --- a/vendor/go.mau.fi/whatsmeow/keepalive.go +++ b/vendor/go.mau.fi/whatsmeow/keepalive.go @@ -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 } diff --git a/vendor/go.mau.fi/whatsmeow/mediaretry.go b/vendor/go.mau.fi/whatsmeow/mediaretry.go index ddee228e..52b04608 100644 --- a/vendor/go.mau.fi/whatsmeow/mediaretry.go +++ b/vendor/go.mau.fi/whatsmeow/mediaretry.go @@ -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() diff --git a/vendor/go.mau.fi/whatsmeow/message.go b/vendor/go.mau.fi/whatsmeow/message.go index 36ccfcad..24c1c6c6 100644 --- a/vendor/go.mau.fi/whatsmeow/message.go +++ b/vendor/go.mau.fi/whatsmeow/message.go @@ -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") - } - ts, ok := node.Attrs["t"].(string) - if !ok { - return nil, fmt.Errorf("didn't find valid `t` (timestamp) 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() } - 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) { diff --git a/vendor/go.mau.fi/whatsmeow/notification.go b/vendor/go.mau.fi/whatsmeow/notification.go index c05fd4cb..c55764d1 100644 --- a/vendor/go.mau.fi/whatsmeow/notification.go +++ b/vendor/go.mau.fi/whatsmeow/notification.go @@ -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 diff --git a/vendor/go.mau.fi/whatsmeow/pair.go b/vendor/go.mau.fi/whatsmeow/pair.go index 389d6f52..41dfa086 100644 --- a/vendor/go.mau.fi/whatsmeow/pair.go +++ b/vendor/go.mau.fi/whatsmeow/pair.go @@ -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") diff --git a/vendor/go.mau.fi/whatsmeow/presence.go b/vendor/go.mau.fi/whatsmeow/presence.go index 8de1969a..2943c7da 100644 --- a/vendor/go.mau.fi/whatsmeow/presence.go +++ b/vendor/go.mau.fi/whatsmeow/presence.go @@ -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) diff --git a/vendor/go.mau.fi/whatsmeow/receipt.go b/vendor/go.mau.fi/whatsmeow/receipt.go index 0f74926b..3335583c 100644 --- a/vendor/go.mau.fi/whatsmeow/receipt.go +++ b/vendor/go.mau.fi/whatsmeow/receipt.go @@ -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") diff --git a/vendor/go.mau.fi/whatsmeow/request.go b/vendor/go.mau.fi/whatsmeow/request.go index 444ae1ad..e070126b 100644 --- a/vendor/go.mau.fi/whatsmeow/request.go +++ b/vendor/go.mau.fi/whatsmeow/request.go @@ -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 +} diff --git a/vendor/go.mau.fi/whatsmeow/retry.go b/vendor/go.mau.fi/whatsmeow/retry.go index a5a56d8f..24be9c7a 100644 --- a/vendor/go.mau.fi/whatsmeow/retry.go +++ b/vendor/go.mau.fi/whatsmeow/retry.go @@ -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{ - Tag: "message", - Attrs: attrs, - Content: []waBinary.Node{*encrypted}, - } + 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) - } + content = append(content, cli.makeDeviceIdentityNode()) } - err = cli.sendNode(req) + err = cli.sendNode(waBinary.Node{ + Tag: "message", + Attrs: attrs, + Content: content, + }) if err != nil { return fmt.Errorf("failed to send retry message: %w", err) } diff --git a/vendor/go.mau.fi/whatsmeow/send.go b/vendor/go.mau.fi/whatsmeow/send.go index 9e520fb0..7ef6649f 100644 --- a/vendor/go.mau.fi/whatsmeow/send.go +++ b/vendor/go.mau.fi/whatsmeow/send.go @@ -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, - }}, - } - 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" - } + attrs := waBinary.Attrs{ + "id": id, + "type": getTypeFromMessage(message), + "to": to, } + 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) { diff --git a/vendor/go.mau.fi/whatsmeow/store/clientpayload.go b/vendor/go.mau.fi/whatsmeow/store/clientpayload.go index 9a65f515..6ebf5d99 100644 --- a/vendor/go.mau.fi/whatsmeow/store/clientpayload.go +++ b/vendor/go.mau.fi/whatsmeow/store/clientpayload.go @@ -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 diff --git a/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go b/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go index 3150cfec..b7c0a7c4 100644 --- a/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go +++ b/vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go @@ -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 { diff --git a/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go b/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go index b98f2d61..37bd7c29 100644 --- a/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go +++ b/vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go @@ -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 +} diff --git a/vendor/go.mau.fi/whatsmeow/types/events/events.go b/vendor/go.mau.fi/whatsmeow/types/events/events.go index 8b9e9a4f..92ae035f 100644 --- a/vendor/go.mau.fi/whatsmeow/types/events/events.go +++ b/vendor/go.mau.fi/whatsmeow/types/events/events.go @@ -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 diff --git a/vendor/go.mau.fi/whatsmeow/types/user.go b/vendor/go.mau.fi/whatsmeow/types/user.go index 523d70d1..1aff91e8 100644 --- a/vendor/go.mau.fi/whatsmeow/types/user.go +++ b/vendor/go.mau.fi/whatsmeow/types/user.go @@ -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 +} diff --git a/vendor/go.mau.fi/whatsmeow/user.go b/vendor/go.mau.fi/whatsmeow/user.go index fea80521..bbddd405 100644 --- a/vendor/go.mau.fi/whatsmeow/user.go +++ b/vendor/go.mau.fi/whatsmeow/user.go @@ -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 } |