Update dependencies and remove old matterclient lib (#2067)

This commit is contained in:
Wim
2023-08-05 20:43:19 +02:00
committed by GitHub
parent 9459495484
commit 56e7bd01ca
772 changed files with 139315 additions and 121315 deletions

View File

@@ -0,0 +1,13 @@
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yaml,yml}]
indent_style = space
indent_size = 2

View File

@@ -27,12 +27,11 @@ Most core features are already present:
* Joining via invite messages, using and creating invite links
* Sending and receiving typing notifications
* Sending and receiving delivery and read receipts
* Reading app state (contact list, chat pin/mute status, etc)
* Reading and writing 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 broadcast list messages (this is not supported on WhatsApp web either)
* Calls

View File

@@ -74,7 +74,7 @@ func (cli *Client) FetchAppState(name appstate.WAPatchName, fullSync, onlyIfNotS
}
}
for _, mutation := range mutations {
cli.dispatchAppState(mutation, !fullSync || cli.EmitAppStateEventsOnFullSync)
cli.dispatchAppState(mutation, fullSync, cli.EmitAppStateEventsOnFullSync)
}
}
if fullSync {
@@ -105,7 +105,10 @@ func (cli *Client) filterContacts(mutations []appstate.Mutation) ([]appstate.Mut
return filteredMutations, contacts
}
func (cli *Client) dispatchAppState(mutation appstate.Mutation, dispatchEvts bool) {
func (cli *Client) dispatchAppState(mutation appstate.Mutation, fullSync bool, emitOnFullSync bool) {
dispatchEvts := !fullSync || emitOnFullSync
if mutation.Operation != waProto.SyncdMutation_SET {
return
}
@@ -118,87 +121,108 @@ func (cli *Client) dispatchAppState(mutation appstate.Mutation, dispatchEvts boo
if len(mutation.Index) > 1 {
jid, _ = types.ParseJID(mutation.Index[1])
}
ts := time.Unix(mutation.Action.GetTimestamp(), 0)
ts := time.UnixMilli(mutation.Action.GetTimestamp())
var storeUpdateError error
var eventToDispatch interface{}
switch mutation.Index[0] {
case "mute":
case appstate.IndexMute:
act := mutation.Action.GetMuteAction()
eventToDispatch = &events.Mute{JID: jid, Timestamp: ts, Action: act}
eventToDispatch = &events.Mute{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
var mutedUntil time.Time
if act.GetMuted() {
mutedUntil = time.Unix(act.GetMuteEndTimestamp(), 0)
mutedUntil = time.UnixMilli(act.GetMuteEndTimestamp())
}
if cli.Store.ChatSettings != nil {
storeUpdateError = cli.Store.ChatSettings.PutMutedUntil(jid, mutedUntil)
}
case "pin_v1":
case appstate.IndexPin:
act := mutation.Action.GetPinAction()
eventToDispatch = &events.Pin{JID: jid, Timestamp: ts, Action: act}
eventToDispatch = &events.Pin{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
if cli.Store.ChatSettings != nil {
storeUpdateError = cli.Store.ChatSettings.PutPinned(jid, act.GetPinned())
}
case "archive":
case appstate.IndexArchive:
act := mutation.Action.GetArchiveChatAction()
eventToDispatch = &events.Archive{JID: jid, Timestamp: ts, Action: act}
eventToDispatch = &events.Archive{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
if cli.Store.ChatSettings != nil {
storeUpdateError = cli.Store.ChatSettings.PutArchived(jid, act.GetArchived())
}
case "contact":
case appstate.IndexContact:
act := mutation.Action.GetContactAction()
eventToDispatch = &events.Contact{JID: jid, Timestamp: ts, Action: act}
eventToDispatch = &events.Contact{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
if cli.Store.Contacts != nil {
storeUpdateError = cli.Store.Contacts.PutContactName(jid, act.GetFirstName(), act.GetFullName())
}
case "deleteChat":
case appstate.IndexClearChat:
act := mutation.Action.GetClearChatAction()
eventToDispatch = &events.ClearChat{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
case appstate.IndexDeleteChat:
act := mutation.Action.GetDeleteChatAction()
eventToDispatch = &events.DeleteChat{JID: jid, Timestamp: ts, Action: act}
case "star":
eventToDispatch = &events.DeleteChat{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
case appstate.IndexStar:
if len(mutation.Index) < 5 {
return
}
evt := events.Star{
ChatJID: jid,
MessageID: mutation.Index[2],
Timestamp: ts,
Action: mutation.Action.GetStarAction(),
IsFromMe: mutation.Index[3] == "1",
ChatJID: jid,
MessageID: mutation.Index[2],
Timestamp: ts,
Action: mutation.Action.GetStarAction(),
IsFromMe: mutation.Index[3] == "1",
FromFullSync: fullSync,
}
if mutation.Index[4] != "0" {
evt.SenderJID, _ = types.ParseJID(mutation.Index[4])
}
eventToDispatch = &evt
case "deleteMessageForMe":
case appstate.IndexDeleteMessageForMe:
if len(mutation.Index) < 5 {
return
}
evt := events.DeleteForMe{
ChatJID: jid,
MessageID: mutation.Index[2],
Timestamp: ts,
Action: mutation.Action.GetDeleteMessageForMeAction(),
IsFromMe: mutation.Index[3] == "1",
ChatJID: jid,
MessageID: mutation.Index[2],
Timestamp: ts,
Action: mutation.Action.GetDeleteMessageForMeAction(),
IsFromMe: mutation.Index[3] == "1",
FromFullSync: fullSync,
}
if mutation.Index[4] != "0" {
evt.SenderJID, _ = types.ParseJID(mutation.Index[4])
}
eventToDispatch = &evt
case "markChatAsRead":
case appstate.IndexMarkChatAsRead:
eventToDispatch = &events.MarkChatAsRead{
JID: jid,
Timestamp: ts,
Action: mutation.Action.GetMarkChatAsReadAction(),
JID: jid,
Timestamp: ts,
Action: mutation.Action.GetMarkChatAsReadAction(),
FromFullSync: fullSync,
}
case appstate.IndexSettingPushName:
eventToDispatch = &events.PushNameSetting{
Timestamp: ts,
Action: mutation.Action.GetPushNameSetting(),
FromFullSync: fullSync,
}
case "setting_pushName":
eventToDispatch = &events.PushNameSetting{Timestamp: ts, Action: mutation.Action.GetPushNameSetting()}
cli.Store.PushName = mutation.Action.GetPushNameSetting().GetName()
err := cli.Store.Save()
if err != nil {
cli.Log.Errorf("Failed to save device store after updating push name: %v", err)
}
case "setting_unarchiveChats":
eventToDispatch = &events.UnarchiveChatsSetting{Timestamp: ts, Action: mutation.Action.GetUnarchiveChatsSetting()}
case appstate.IndexSettingUnarchiveChats:
eventToDispatch = &events.UnarchiveChatsSetting{
Timestamp: ts,
Action: mutation.Action.GetUnarchiveChatsSetting(),
FromFullSync: fullSync,
}
case appstate.IndexUserStatusMute:
eventToDispatch = &events.UserStatusMute{
JID: jid,
Timestamp: ts,
Action: mutation.Action.GetUserStatusMuteAction(),
FromFullSync: fullSync,
}
}
if storeUpdateError != nil {
cli.Log.Errorf("Failed to update device store after app state mutation: %v", storeUpdateError)
@@ -280,3 +304,63 @@ func (cli *Client) requestAppStateKeys(ctx context.Context, rawKeyIDs [][]byte)
cli.Log.Warnf("Failed to send app state key request: %v", err)
}
}
// SendAppState sends the given app state patch, then resyncs that app state type from the server
// to update local caches and send events for the updates.
//
// You can use the Build methods in the appstate package to build the parameter for this method, e.g.
//
// cli.SendAppState(appstate.BuildMute(targetJID, true, 24 * time.Hour))
func (cli *Client) SendAppState(patch appstate.PatchInfo) error {
version, hash, err := cli.Store.AppState.GetAppStateVersion(string(patch.Type))
if err != nil {
return err
}
// TODO create new key instead of reusing the primary client's keys
latestKeyID, err := cli.Store.AppStateKeys.GetLatestAppStateSyncKeyID()
if err != nil {
return fmt.Errorf("failed to get latest app state key ID: %w", err)
} else if latestKeyID == nil {
return fmt.Errorf("no app state keys found, creating app state keys is not yet supported")
}
state := appstate.HashState{Version: version, Hash: hash}
encodedPatch, err := cli.appStateProc.EncodePatch(latestKeyID, state, patch)
if err != nil {
return err
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:sync:app:state",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "sync",
Content: []waBinary.Node{{
Tag: "collection",
Attrs: waBinary.Attrs{
"name": string(patch.Type),
"version": version,
"return_snapshot": false,
},
Content: []waBinary.Node{{
Tag: "patch",
Content: encodedPatch,
}},
}},
}},
})
if err != nil {
return err
}
respCollection := resp.GetChildByTag("sync", "collection")
respCollectionAttr := respCollection.AttrGetter()
if respCollectionAttr.OptionalString("type") == "error" {
// TODO parse error properly
return fmt.Errorf("%w: %s", ErrAppStateUpdate, respCollection.XMLString())
}
return cli.FetchAppState(patch.Type, false, false)
}

View File

@@ -290,7 +290,7 @@ func (proc *Processor) DecodePatches(list *PatchList, initialState HashState, va
if err != nil {
return
}
patchMAC := generatePatchMAC(patch, list.Name, keys.PatchMAC)
patchMAC := generatePatchMAC(patch, list.Name, keys.PatchMAC, patch.GetVersion().GetVersion())
if !bytes.Equal(patchMAC, patch.GetPatchMac()) {
err = fmt.Errorf("failed to verify patch v%d: %w", version, ErrMismatchingPatchMAC)
return

View File

@@ -0,0 +1,200 @@
package appstate
import (
"crypto/sha256"
"encoding/json"
"fmt"
"time"
"google.golang.org/protobuf/proto"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/cbcutil"
)
// MutationInfo contains information about a single mutation to the app state.
type MutationInfo struct {
// Index contains the thing being mutated (like `mute` or `pin_v1`), followed by parameters like the target JID.
Index []string
// Version is a static number that depends on the thing being mutated.
Version int32
// Value contains the data for the mutation.
Value *waProto.SyncActionValue
}
// PatchInfo contains information about a patch to the app state.
// A patch can contain multiple mutations, as long as all mutations are in the same app state type.
type PatchInfo struct {
// Timestamp is the time when the patch was created. This will be filled automatically in EncodePatch if it's zero.
Timestamp time.Time
// Type is the app state type being mutated.
Type WAPatchName
// Mutations contains the individual mutations to apply to the app state in this patch.
Mutations []MutationInfo
}
// BuildMute builds an app state patch for muting or unmuting a chat.
//
// If mute is true and the mute duration is zero, the chat is muted forever.
func BuildMute(target types.JID, mute bool, muteDuration time.Duration) PatchInfo {
var muteEndTimestamp *int64
if muteDuration > 0 {
muteEndTimestamp = proto.Int64(time.Now().Add(muteDuration).UnixMilli())
}
return PatchInfo{
Type: WAPatchRegularHigh,
Mutations: []MutationInfo{{
Index: []string{IndexMute, target.String()},
Version: 2,
Value: &waProto.SyncActionValue{
MuteAction: &waProto.MuteAction{
Muted: proto.Bool(mute),
MuteEndTimestamp: muteEndTimestamp,
},
},
}},
}
}
func newPinMutationInfo(target types.JID, pin bool) MutationInfo {
return MutationInfo{
Index: []string{IndexPin, target.String()},
Version: 5,
Value: &waProto.SyncActionValue{
PinAction: &waProto.PinAction{
Pinned: &pin,
},
},
}
}
// BuildPin builds an app state patch for pinning or unpinning a chat.
func BuildPin(target types.JID, pin bool) PatchInfo {
return PatchInfo{
Type: WAPatchRegularLow,
Mutations: []MutationInfo{
newPinMutationInfo(target, pin),
},
}
}
// BuildArchive builds an app state patch for archiving or unarchiving a chat.
//
// The last message timestamp and last message key are optional and can be set to zero values (`time.Time{}` and `nil`).
//
// Archiving a chat will also unpin it automatically.
func BuildArchive(target types.JID, archive bool, lastMessageTimestamp time.Time, lastMessageKey *waProto.MessageKey) PatchInfo {
if lastMessageTimestamp.IsZero() {
lastMessageTimestamp = time.Now()
}
archiveMutationInfo := MutationInfo{
Index: []string{IndexArchive, target.String()},
Version: 3,
Value: &waProto.SyncActionValue{
ArchiveChatAction: &waProto.ArchiveChatAction{
Archived: &archive,
MessageRange: &waProto.SyncActionMessageRange{
LastMessageTimestamp: proto.Int64(lastMessageTimestamp.Unix()),
// TODO set LastSystemMessageTimestamp?
},
},
},
}
if lastMessageKey != nil {
archiveMutationInfo.Value.ArchiveChatAction.MessageRange.Messages = []*waProto.SyncActionMessage{{
Key: lastMessageKey,
Timestamp: proto.Int64(lastMessageTimestamp.Unix()),
}}
}
mutations := []MutationInfo{archiveMutationInfo}
if archive {
mutations = append(mutations, newPinMutationInfo(target, false))
}
result := PatchInfo{
Type: WAPatchRegularLow,
Mutations: mutations,
}
return result
}
func (proc *Processor) EncodePatch(keyID []byte, state HashState, patchInfo PatchInfo) ([]byte, error) {
keys, err := proc.getAppStateKey(keyID)
if err != nil {
return nil, fmt.Errorf("failed to get app state key details with key ID %x: %w", keyID, err)
}
if patchInfo.Timestamp.IsZero() {
patchInfo.Timestamp = time.Now()
}
mutations := make([]*waProto.SyncdMutation, 0, len(patchInfo.Mutations))
for _, mutationInfo := range patchInfo.Mutations {
mutationInfo.Value.Timestamp = proto.Int64(patchInfo.Timestamp.UnixMilli())
indexBytes, err := json.Marshal(mutationInfo.Index)
if err != nil {
return nil, fmt.Errorf("failed to marshal mutation index: %w", err)
}
pbObj := &waProto.SyncActionData{
Index: indexBytes,
Value: mutationInfo.Value,
Padding: []byte{},
Version: &mutationInfo.Version,
}
content, err := proto.Marshal(pbObj)
if err != nil {
return nil, fmt.Errorf("failed to marshal mutation: %w", err)
}
encryptedContent, err := cbcutil.Encrypt(keys.ValueEncryption, nil, content)
if err != nil {
return nil, fmt.Errorf("failed to encrypt mutation: %w", err)
}
valueMac := generateContentMAC(waProto.SyncdMutation_SET, encryptedContent, keyID, keys.ValueMAC)
indexMac := concatAndHMAC(sha256.New, keys.Index, indexBytes)
mutations = append(mutations, &waProto.SyncdMutation{
Operation: waProto.SyncdMutation_SET.Enum(),
Record: &waProto.SyncdRecord{
Index: &waProto.SyncdIndex{Blob: indexMac},
Value: &waProto.SyncdValue{Blob: append(encryptedContent, valueMac...)},
KeyId: &waProto.KeyId{Id: keyID},
},
})
}
warn, err := state.updateHash(mutations, func(indexMAC []byte, _ int) ([]byte, error) {
return proc.Store.AppState.GetAppStateMutationMAC(string(patchInfo.Type), indexMAC)
})
if len(warn) > 0 {
proc.Log.Warnf("Warnings while updating hash for %s (sending new app state): %+v", patchInfo.Type, warn)
}
if err != nil {
return nil, fmt.Errorf("failed to update state hash: %w", err)
}
state.Version += 1
syncdPatch := &waProto.SyncdPatch{
SnapshotMac: state.generateSnapshotMAC(patchInfo.Type, keys.SnapshotMAC),
KeyId: &waProto.KeyId{Id: keyID},
Mutations: mutations,
}
syncdPatch.PatchMac = generatePatchMAC(syncdPatch, patchInfo.Type, keys.PatchMAC, state.Version)
result, err := proto.Marshal(syncdPatch)
if err != nil {
return nil, fmt.Errorf("failed to marshal compiled patch: %w", err)
}
return result, nil
}

View File

@@ -77,14 +77,14 @@ func (hs *HashState) generateSnapshotMAC(name WAPatchName, key []byte) []byte {
return concatAndHMAC(sha256.New, key, hs.Hash[:], uint64ToBytes(hs.Version), []byte(name))
}
func generatePatchMAC(patch *waProto.SyncdPatch, name WAPatchName, key []byte) []byte {
func generatePatchMAC(patch *waProto.SyncdPatch, name WAPatchName, key []byte, version uint64) []byte {
dataToHash := make([][]byte, len(patch.GetMutations())+3)
dataToHash[0] = patch.GetSnapshotMac()
for i, mutation := range patch.Mutations {
val := mutation.GetRecord().GetValue().GetBlob()
dataToHash[i+1] = val[len(val)-32:]
}
dataToHash[len(dataToHash)-2] = uint64ToBytes(patch.GetVersion().GetVersion())
dataToHash[len(dataToHash)-2] = uint64ToBytes(version)
dataToHash[len(dataToHash)-1] = []byte(name)
return concatAndHMAC(sha256.New, key, dataToHash...)
}

View File

@@ -35,6 +35,22 @@ const (
// AllPatchNames contains all currently known patch state names.
var AllPatchNames = [...]WAPatchName{WAPatchCriticalBlock, WAPatchCriticalUnblockLow, WAPatchRegularHigh, WAPatchRegular, WAPatchRegularLow}
// Constants for the first part of app state indexes.
const (
IndexMute = "mute"
IndexPin = "pin_v1"
IndexArchive = "archive"
IndexContact = "contact"
IndexClearChat = "clearChat"
IndexDeleteChat = "deleteChat"
IndexStar = "star"
IndexDeleteMessageForMe = "deleteMessageForMe"
IndexMarkChatAsRead = "markChatAsRead"
IndexSettingPushName = "setting_pushName"
IndexSettingUnarchiveChats = "setting_unarchiveChats"
IndexUserStatusMute = "userStatusMute"
)
type Processor struct {
keyCache map[string]ExpandedAppStateKeys
keyCacheLock sync.Mutex

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ message ADVSignedDeviceIdentity {
message ADVSignedDeviceIdentityHMAC {
optional bytes details = 1;
optional bytes hmac = 2;
optional ADVEncryptionType accountType = 3;
}
message ADVKeyIndexList {
@@ -23,12 +24,19 @@ message ADVKeyIndexList {
optional uint64 timestamp = 2;
optional uint32 currentIndex = 3;
repeated uint32 validIndexes = 4 [packed=true];
optional ADVEncryptionType accountType = 5;
}
enum ADVEncryptionType {
E2EE = 0;
HOSTED = 1;
}
message ADVDeviceIdentity {
optional uint32 rawId = 1;
optional uint64 timestamp = 2;
optional uint32 keyIndex = 3;
optional ADVEncryptionType accountType = 4;
optional ADVEncryptionType deviceType = 5;
}
message DeviceProps {
@@ -51,11 +59,18 @@ message DeviceProps {
IOS_CATALYST = 15;
ANDROID_PHONE = 16;
ANDROID_AMBIGUOUS = 17;
WEAR_OS = 18;
AR_WRIST = 19;
AR_DEVICE = 20;
UWP = 21;
VR = 22;
}
message HistorySyncConfig {
optional uint32 fullSyncDaysLimit = 1;
optional uint32 fullSyncSizeMbLimit = 2;
optional uint32 storageQuotaMb = 3;
optional bool inlineInitialPayloadInE2EeMsg = 4;
optional uint32 recentSyncDaysLimit = 5;
}
message AppVersion {
@@ -73,76 +88,6 @@ message DeviceProps {
optional HistorySyncConfig historySyncConfig = 5;
}
message PeerDataOperationRequestMessage {
message RequestUrlPreview {
optional string url = 1;
}
message RequestStickerReupload {
optional string fileSha256 = 1;
}
message HistorySyncOnDemandRequest {
optional string chatJid = 1;
optional string oldestMsgId = 2;
optional bool oldestMsgFromMe = 3;
optional int32 onDemandMsgCount = 4;
optional int64 oldestMsgTimestampMs = 5;
}
optional PeerDataOperationRequestType peerDataOperationRequestType = 1;
repeated RequestStickerReupload requestStickerReupload = 2;
repeated RequestUrlPreview requestUrlPreview = 3;
optional HistorySyncOnDemandRequest historySyncOnDemandRequest = 4;
}
message PaymentInviteMessage {
enum ServiceType {
UNKNOWN = 0;
FBPAY = 1;
NOVI = 2;
UPI = 3;
}
optional ServiceType serviceType = 1;
optional int64 expiryTimestamp = 2;
}
message OrderMessage {
enum OrderSurface {
CATALOG = 1;
}
enum OrderStatus {
INQUIRY = 1;
}
optional string orderId = 1;
optional bytes thumbnail = 2;
optional int32 itemCount = 3;
optional OrderStatus status = 4;
optional OrderSurface surface = 5;
optional string message = 6;
optional string orderTitle = 7;
optional string sellerJid = 8;
optional string token = 9;
optional int64 totalAmount1000 = 10;
optional string totalCurrencyCode = 11;
optional ContextInfo contextInfo = 17;
}
message LocationMessage {
optional double degreesLatitude = 1;
optional double degreesLongitude = 2;
optional string name = 3;
optional string address = 4;
optional string url = 5;
optional bool isLive = 6;
optional uint32 accuracyInMeters = 7;
optional float speedInMps = 8;
optional uint32 degreesClockwiseFromMagneticNorth = 9;
optional string comment = 11;
optional bytes jpegThumbnail = 16;
optional ContextInfo contextInfo = 17;
}
message LiveLocationMessage {
optional double degreesLatitude = 1;
optional double degreesLongitude = 2;
@@ -250,7 +195,12 @@ message InteractiveResponseMessage {
}
message Body {
enum Format {
DEFAULT = 0;
EXTENSIONS_1 = 1;
}
optional string text = 1;
optional Format format = 2;
}
optional Body body = 1;
@@ -293,6 +243,7 @@ message InteractiveMessage {
ImageMessage imageMessage = 4;
bytes jpegThumbnail = 6;
VideoMessage videoMessage = 7;
LocationMessage locationMessage = 8;
}
}
@@ -306,6 +257,11 @@ message InteractiveMessage {
optional int32 messageVersion = 3;
}
message CarouselMessage {
repeated InteractiveMessage cards = 1;
optional int32 messageVersion = 2;
}
message Body {
optional string text = 1;
}
@@ -318,6 +274,7 @@ message InteractiveMessage {
ShopMessage shopStorefrontMessage = 4;
CollectionMessage collectionMessage = 5;
NativeFlowMessage nativeFlowMessage = 6;
CarouselMessage carouselMessage = 7;
}
}
@@ -374,6 +331,8 @@ message HistorySyncNotification {
optional string originalMessageId = 8;
optional uint32 progress = 9;
optional int64 oldestMsgInChunkTimestampSec = 10;
optional bytes initialHistBootstrapInlinePayload = 11;
optional string peerDataRequestSessionId = 12;
}
message HighlyStructuredMessage {
@@ -466,13 +425,10 @@ message ExtendedTextMessage {
DEFAULT_SUB = 3;
}
enum FontType {
SANS_SERIF = 0;
SERIF = 1;
NORICAN_REGULAR = 2;
BRYNDAN_WRITE = 3;
BEBASNEUE_REGULAR = 4;
OSWALD_HEAVY = 5;
DAMION_REGULAR = 6;
SYSTEM = 0;
SYSTEM_TEXT = 1;
FB_SCRIPT = 2;
SYSTEM_BOLD = 6;
MORNINGBREEZE_REGULAR = 7;
CALISTOGA_REGULAR = 8;
EXO2_EXTRABOLD = 9;
@@ -510,6 +466,12 @@ message EncReactionMessage {
optional bytes encIv = 3;
}
message EncCommentMessage {
optional MessageKey targetMessageKey = 1;
optional bytes encPayload = 2;
optional bytes encIv = 3;
}
message DocumentMessage {
optional string url = 1;
optional string mimetype = 2;
@@ -629,6 +591,21 @@ message ButtonsMessage {
}
}
message BotFeedbackMessage {
enum BotFeedbackKind {
BOT_FEEDBACK_POSITIVE = 0;
BOT_FEEDBACK_NEGATIVE_GENERIC = 1;
BOT_FEEDBACK_NEGATIVE_HELPFUL = 2;
BOT_FEEDBACK_NEGATIVE_INTERESTING = 3;
BOT_FEEDBACK_NEGATIVE_ACCURATE = 4;
BOT_FEEDBACK_NEGATIVE_SAFE = 5;
BOT_FEEDBACK_NEGATIVE_OTHER = 6;
}
optional MessageKey messageKey = 1;
optional BotFeedbackKind kind = 2;
optional string text = 3;
}
message AudioMessage {
optional string url = 1;
optional string mimetype = 2;
@@ -729,12 +706,20 @@ message GroupMention {
}
message DisappearingMode {
enum Trigger {
UNKNOWN = 0;
CHAT_SETTING = 1;
ACCOUNT_SETTING = 2;
BULK_CHANGE = 3;
}
enum Initiator {
CHANGED_IN_CHAT = 0;
INITIATED_BY_ME = 1;
INITIATED_BY_OTHER = 2;
}
optional Initiator initiator = 1;
optional Trigger trigger = 2;
optional string initiatorDeviceJid = 3;
}
message DeviceListMetadata {
@@ -752,6 +737,12 @@ message ContextInfo {
optional string utmCampaign = 2;
}
message ForwardedNewsletterMessageInfo {
optional string newsletterJid = 1;
optional int32 serverMessageId = 2;
optional string newsletterName = 3;
}
message ExternalAdReplyInfo {
enum MediaType {
NONE = 0;
@@ -771,6 +762,11 @@ message ContextInfo {
optional bool renderLargerThumbnail = 11;
optional bool showAdAttribution = 12;
optional string ctwaClid = 13;
optional string ref = 14;
}
message BusinessMessageForwardInfo {
optional string businessOwnerJid = 1;
}
message AdReplyInfo {
@@ -813,6 +809,27 @@ message ContextInfo {
optional bool isSampled = 39;
repeated GroupMention groupMentions = 40;
optional UTMInfo utm = 41;
optional ForwardedNewsletterMessageInfo forwardedNewsletterMessageInfo = 43;
optional BusinessMessageForwardInfo businessMessageForwardInfo = 44;
optional string smbClientCampaignId = 45;
}
message BotPluginMetadata {
optional bool isPlaceholder = 1;
}
message BotMetadata {
optional BotAvatarMetadata avatarMetadata = 1;
optional string personaId = 2;
optional BotPluginMetadata pluginMetadata = 3;
}
message BotAvatarMetadata {
optional uint32 sentiment = 1;
optional string behaviorGraph = 2;
optional uint32 action = 3;
optional uint32 intensity = 4;
optional uint32 wordCount = 5;
}
message ActionLink {
@@ -935,10 +952,18 @@ message Message {
optional PollCreationMessage pollCreationMessageV2 = 60;
optional ScheduledCallCreationMessage scheduledCallCreationMessage = 61;
optional FutureProofMessage groupMentionedMessage = 62;
optional PinMessage pinMessage = 63;
optional PinInChatMessage pinInChatMessage = 63;
optional PollCreationMessage pollCreationMessageV3 = 64;
optional ScheduledCallEditMessage scheduledCallEditMessage = 65;
optional VideoMessage ptvMessage = 66;
optional FutureProofMessage botInvokeMessage = 67;
optional EncCommentMessage encCommentMessage = 68;
}
message MessageSecretMessage {
optional sfixed32 version = 1;
optional bytes encIv = 2;
optional bytes encPayload = 3;
}
message MessageContextInfo {
@@ -946,6 +971,9 @@ message MessageContextInfo {
optional int32 deviceListMetadataVersion = 2;
optional bytes messageSecret = 3;
optional bytes paddingBytes = 4;
optional uint32 messageAddOnDurationInSecs = 5;
optional bytes botMessageSecret = 6;
optional BotMetadata botMetadata = 7;
}
message VideoMessage {
@@ -1048,6 +1076,7 @@ message StickerMessage {
optional ContextInfo contextInfo = 17;
optional int64 stickerSentTs = 18;
optional bool isAvatar = 19;
optional bool isAiSticker = 20;
}
message SenderKeyDistributionMessage {
@@ -1117,6 +1146,8 @@ message ProtocolMessage {
MESSAGE_EDIT = 14;
PEER_DATA_OPERATION_REQUEST_MESSAGE = 16;
PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE = 17;
REQUEST_WELCOME_MESSAGE = 18;
BOT_FEEDBACK_MESSAGE = 19;
}
optional MessageKey key = 1;
optional Type type = 2;
@@ -1132,6 +1163,7 @@ message ProtocolMessage {
optional int64 timestampMs = 15;
optional PeerDataOperationRequestMessage peerDataOperationRequestMessage = 16;
optional PeerDataOperationRequestResponseMessage peerDataOperationRequestResponseMessage = 17;
optional BotFeedbackMessage botFeedbackMessage = 18;
}
message ProductMessage {
@@ -1194,14 +1226,14 @@ message PollCreationMessage {
optional ContextInfo contextInfo = 5;
}
message PinMessage {
enum PinMessageType {
UNKNOWN_PIN_MESSAGE_TYPE = 0;
message PinInChatMessage {
enum Type {
UNKNOWN_TYPE = 0;
PIN_FOR_ALL = 1;
UNPIN_FOR_ALL = 2;
}
optional MessageKey key = 1;
optional PinMessageType pinMessageType = 2;
optional Type type = 2;
optional int64 senderTimestampMs = 3;
}
@@ -1210,10 +1242,25 @@ enum PeerDataOperationRequestType {
SEND_RECENT_STICKER_BOOTSTRAP = 1;
GENERATE_LINK_PREVIEW = 2;
HISTORY_SYNC_ON_DEMAND = 3;
PLACEHOLDER_MESSAGE_RESEND = 4;
}
message PeerDataOperationRequestResponseMessage {
message PeerDataOperationResult {
message PlaceholderMessageResendResponse {
optional bytes webMessageInfoBytes = 1;
}
message LinkPreviewResponse {
message LinkPreviewHighQualityThumbnail {
optional string directPath = 1;
optional string thumbHash = 2;
optional string encThumbHash = 3;
optional bytes mediaKey = 4;
optional int64 mediaKeyTimestampMs = 5;
optional int32 thumbWidth = 6;
optional int32 thumbHeight = 7;
}
optional string url = 1;
optional string title = 2;
optional string description = 3;
@@ -1221,11 +1268,13 @@ message PeerDataOperationRequestResponseMessage {
optional string canonicalUrl = 5;
optional string matchText = 6;
optional string previewType = 7;
optional LinkPreviewHighQualityThumbnail hqThumbnail = 8;
}
optional MediaRetryNotification.ResultType mediaUploadResult = 1;
optional StickerMessage stickerMessage = 2;
optional LinkPreviewResponse linkPreviewResponse = 3;
optional PlaceholderMessageResendResponse placeholderMessageResendResponse = 4;
}
optional PeerDataOperationRequestType peerDataOperationRequestType = 1;
@@ -1233,6 +1282,82 @@ message PeerDataOperationRequestResponseMessage {
repeated PeerDataOperationResult peerDataOperationResult = 3;
}
message PeerDataOperationRequestMessage {
message RequestUrlPreview {
optional string url = 1;
optional bool includeHqThumbnail = 2;
}
message RequestStickerReupload {
optional string fileSha256 = 1;
}
message PlaceholderMessageResendRequest {
optional MessageKey messageKey = 1;
}
message HistorySyncOnDemandRequest {
optional string chatJid = 1;
optional string oldestMsgId = 2;
optional bool oldestMsgFromMe = 3;
optional int32 onDemandMsgCount = 4;
optional int64 oldestMsgTimestampMs = 5;
}
optional PeerDataOperationRequestType peerDataOperationRequestType = 1;
repeated RequestStickerReupload requestStickerReupload = 2;
repeated RequestUrlPreview requestUrlPreview = 3;
optional HistorySyncOnDemandRequest historySyncOnDemandRequest = 4;
repeated PlaceholderMessageResendRequest placeholderMessageResendRequest = 5;
}
message PaymentInviteMessage {
enum ServiceType {
UNKNOWN = 0;
FBPAY = 1;
NOVI = 2;
UPI = 3;
}
optional ServiceType serviceType = 1;
optional int64 expiryTimestamp = 2;
}
message OrderMessage {
enum OrderSurface {
CATALOG = 1;
}
enum OrderStatus {
INQUIRY = 1;
}
optional string orderId = 1;
optional bytes thumbnail = 2;
optional int32 itemCount = 3;
optional OrderStatus status = 4;
optional OrderSurface surface = 5;
optional string message = 6;
optional string orderTitle = 7;
optional string sellerJid = 8;
optional string token = 9;
optional int64 totalAmount1000 = 10;
optional string totalCurrencyCode = 11;
optional ContextInfo contextInfo = 17;
}
message LocationMessage {
optional double degreesLatitude = 1;
optional double degreesLongitude = 2;
optional string name = 3;
optional string address = 4;
optional string url = 5;
optional bool isLive = 6;
optional uint32 accuracyInMeters = 7;
optional float speedInMps = 8;
optional uint32 degreesClockwiseFromMagneticNorth = 9;
optional string comment = 11;
optional bytes jpegThumbnail = 16;
optional ContextInfo contextInfo = 17;
}
message EphemeralSetting {
optional sfixed32 duration = 1;
optional sfixed64 timestamp = 2;
@@ -1277,6 +1402,15 @@ message PastParticipant {
optional uint64 leaveTs = 3;
}
message NotificationSettings {
optional string messageVibrate = 1;
optional string messagePopup = 2;
optional string messageLight = 3;
optional bool lowPriorityNotifications = 4;
optional bool reactionsMuted = 5;
optional string callVibrate = 6;
}
enum MediaVisibility {
DEFAULT = 0;
OFF = 1;
@@ -1332,12 +1466,20 @@ message GlobalSettings {
optional int32 disappearingModeDuration = 9;
optional int64 disappearingModeTimestamp = 10;
optional AvatarUserSettings avatarUserSettings = 11;
optional int32 fontSize = 12;
optional bool securityNotifications = 13;
optional bool autoUnarchiveChats = 14;
optional int32 videoQualityMode = 15;
optional int32 photoQualityMode = 16;
optional NotificationSettings individualNotificationSettings = 17;
optional NotificationSettings groupNotificationSettings = 18;
}
message Conversation {
enum EndOfHistoryTransferType {
COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY = 0;
COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY = 1;
COMPLETE_ON_DEMAND_SYNC_BUT_MORE_MSG_REMAIN_ON_PRIMARY = 2;
}
required string id = 1;
repeated HistorySyncMsg messages = 2;
@@ -1395,50 +1537,6 @@ message AutoDownloadSettings {
optional bool downloadDocuments = 4;
}
// Duplicate type omitted
//message PollEncValue {
// optional bytes encPayload = 1;
// optional bytes encIv = 2;
//}
message MsgRowOpaqueData {
optional MsgOpaqueData currentMsg = 1;
optional MsgOpaqueData quotedMsg = 2;
}
message MsgOpaqueData {
message PollOption {
optional string name = 1;
}
optional string body = 1;
optional string caption = 3;
optional double lng = 5;
optional bool isLive = 6;
optional double lat = 7;
optional int32 paymentAmount1000 = 8;
optional string paymentNoteMsgBody = 9;
optional string canonicalUrl = 10;
optional string matchedText = 11;
optional string title = 12;
optional string description = 13;
optional bytes futureproofBuffer = 14;
optional string clientUrl = 15;
optional string loc = 16;
optional string pollName = 17;
repeated PollOption pollOptions = 18;
optional uint32 pollSelectableOptionsCount = 20;
optional bytes messageSecret = 21;
optional string originalSelfAuthor = 51;
optional int64 senderTimestampMs = 22;
optional string pollUpdateParentKey = 23;
optional PollEncValue encPollVote = 24;
optional bool isSentCagPollCreation = 28;
optional string encReactionTargetMessageKey = 25;
optional bytes encReactionEncPayload = 26;
optional bytes encReactionEncIv = 27;
}
message ServerErrorReceipt {
optional string stanzaId = 1;
}
@@ -1570,6 +1668,10 @@ message SyncActionValue {
optional ChatAssignmentAction chatAssignment = 35;
optional ChatAssignmentOpenedStatusAction chatAssignmentOpenedStatus = 36;
optional PnForLidChatAction pnForLidChatAction = 37;
optional MarketingMessageAction marketingMessageAction = 38;
optional MarketingMessageBroadcastAction marketingMessageBroadcastAction = 39;
optional ExternalWebBetaAction externalWebBetaAction = 40;
optional PrivacySettingRelayAllCalls privacySettingRelayAllCalls = 41;
}
message UserStatusMuteAction {
@@ -1642,6 +1744,10 @@ message PushNameSetting {
optional string name = 1;
}
message PrivacySettingRelayAllCalls {
optional bool isEnabled = 1;
}
message PrimaryVersionAction {
optional string version = 1;
}
@@ -1668,6 +1774,23 @@ message MuteAction {
optional bool autoMuted = 3;
}
message MarketingMessageBroadcastAction {
optional int32 repliedCount = 1;
}
message MarketingMessageAction {
enum MarketingMessagePrototypeType {
PERSONALIZED = 0;
}
optional string name = 1;
optional string message = 2;
optional MarketingMessagePrototypeType type = 3;
optional int64 createdAt = 4;
optional int64 lastSentAt = 5;
optional bool isDeleted = 6;
optional string mediaId = 7;
}
message MarkChatAsReadAction {
optional bool read = 1;
optional SyncActionMessageRange messageRange = 2;
@@ -1692,6 +1815,10 @@ message KeyExpiration {
optional int32 expiredKeyEpoch = 1;
}
message ExternalWebBetaAction {
optional bool isOptIn = 1;
}
message DeleteMessageForMeAction {
optional bool deleteMedia = 1;
optional int64 messageTimestamp = 2;
@@ -1903,6 +2030,14 @@ message ClientPayload {
ARDEVICE = 30;
VRDEVICE = 31;
BLUE_WEB = 32;
IPAD = 33;
}
enum DeviceType {
PHONE = 0;
TABLET = 1;
DESKTOP = 2;
WEARABLE = 3;
VR = 4;
}
message AppVersion {
optional uint32 primary = 1;
@@ -1925,12 +2060,21 @@ message ClientPayload {
optional string localeLanguageIso6391 = 11;
optional string localeCountryIso31661Alpha2 = 12;
optional string deviceBoard = 13;
optional string deviceExpId = 14;
optional DeviceType deviceType = 15;
}
enum Product {
WHATSAPP = 0;
MESSENGER = 1;
INTEROP = 2;
}
message InteropData {
optional uint64 accountId = 1;
optional uint32 integratorId = 2;
optional bytes token = 3;
}
enum IOSAppExtension {
SHARE_EXTENSION = 0;
SERVICE_EXTENSION = 1;
@@ -1983,6 +2127,7 @@ message ClientPayload {
ERROR_RECONNECT = 3;
NETWORK_SWITCH = 4;
PING_RECONNECT = 5;
UNKNOWN = 6;
}
optional uint64 username = 1;
optional bool passive = 3;
@@ -2010,6 +2155,7 @@ message ClientPayload {
optional bytes paddingBytes = 34;
optional int32 yearClass = 36;
optional int32 memClass = 37;
optional InteropData interopData = 38;
}
message WebNotificationsInfo {
@@ -2183,6 +2329,30 @@ message WebMessageInfo {
CAG_INVITE_AUTO_ADD = 159;
BIZ_CHAT_ASSIGNMENT_UNASSIGN = 160;
CAG_INVITE_AUTO_JOINED = 161;
SCHEDULED_CALL_START_MESSAGE = 162;
COMMUNITY_INVITE_RICH = 163;
COMMUNITY_INVITE_AUTO_ADD_RICH = 164;
SUB_GROUP_INVITE_RICH = 165;
SUB_GROUP_PARTICIPANT_ADD_RICH = 166;
COMMUNITY_LINK_PARENT_GROUP_RICH = 167;
COMMUNITY_PARTICIPANT_ADD_RICH = 168;
SILENCED_UNKNOWN_CALLER_AUDIO = 169;
SILENCED_UNKNOWN_CALLER_VIDEO = 170;
GROUP_MEMBER_ADD_MODE = 171;
GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD = 172;
COMMUNITY_CHANGE_DESCRIPTION = 173;
SENDER_INVITE = 174;
RECEIVER_INVITE = 175;
COMMUNITY_ALLOW_MEMBER_ADDED_GROUPS = 176;
PINNED_MESSAGE_IN_CHAT = 177;
PAYMENT_INVITE_SETUP_INVITER = 178;
PAYMENT_INVITE_SETUP_INVITEE_RECEIVE_ONLY = 179;
PAYMENT_INVITE_SETUP_INVITEE_SEND_AND_RECEIVE = 180;
LINKED_GROUP_CALL_START = 181;
REPORT_TO_ADMIN_ENABLED_STATUS = 182;
EMPTY_SUBGROUP_CREATE = 183;
SCHEDULED_CALL_CANCEL = 184;
SUBGROUP_ADMIN_TRIGGERED_AUTO_ADD_RICH = 185;
}
enum Status {
ERROR = 0;
@@ -2241,6 +2411,7 @@ message WebMessageInfo {
optional KeepInChat keepInChat = 50;
optional string originalSelfAuthorUserJidString = 51;
optional uint64 revokeMessageTimestamp = 52;
optional PinInChat pinInChat = 54;
}
message WebFeatures {
@@ -2331,6 +2502,19 @@ message PollAdditionalMetadata {
optional bool pollInvalidated = 1;
}
message PinInChat {
enum Type {
UNKNOWN_TYPE = 0;
PIN_FOR_ALL = 1;
UNPIN_FOR_ALL = 2;
}
optional Type type = 1;
optional MessageKey key = 2;
optional int64 senderTimestampMs = 3;
optional int64 serverTimestampMs = 4;
optional MessageAddOnContextInfo messageAddOnContextInfo = 5;
}
message PhotoChange {
optional bytes oldPhoto = 1;
optional bytes newPhoto = 2;
@@ -2412,6 +2596,10 @@ message NotificationMessageInfo {
optional string participant = 4;
}
message MessageAddOnContextInfo {
optional uint32 messageAddOnDurationInSecs = 1;
}
message MediaData {
optional string localPath = 1;
}

View File

@@ -9,7 +9,6 @@ package whatsmeow
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
@@ -29,6 +28,7 @@ import (
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
"go.mau.fi/whatsmeow/util/randbytes"
)
// EventHandler is a function that can handle events from WhatsApp.
@@ -65,6 +65,10 @@ type Client struct {
// even when re-syncing the whole state.
EmitAppStateEventsOnFullSync bool
AutomaticMessageRerequestFromPhone bool
pendingPhoneRerequests map[types.MessageID]context.CancelFunc
pendingPhoneRerequestsLock sync.RWMutex
appStateProc *appstate.Processor
appStateSyncLock sync.Mutex
@@ -130,6 +134,8 @@ type Client struct {
// Should SubscribePresence return an error if no privacy token is stored for the user?
ErrorOnSubscribePresenceWithoutToken bool
phoneLinkingCache *phoneLinkingCache
uniqueID string
idCounter uint32
@@ -161,8 +167,7 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
if log == nil {
log = waLog.Noop
}
randomBytes := make([]byte, 2)
_, _ = rand.Read(randomBytes)
uniqueIDPrefix := randbytes.Make(2)
cli := &Client{
http: &http.Client{
Transport: (http.DefaultTransport.(*http.Transport)).Clone(),
@@ -172,7 +177,7 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
Log: log,
recvLog: log.Sub("Recv"),
sendLog: log.Sub("Send"),
uniqueID: fmt.Sprintf("%d.%d-", randomBytes[0], randomBytes[1]),
uniqueID: fmt.Sprintf("%d.%d-", uniqueIDPrefix[0], uniqueIDPrefix[1]),
responseWaiters: make(map[string]chan<- *waBinary.Node),
eventHandlers: make([]wrappedEventHandler, 0, 1),
messageRetries: make(map[string]int),
@@ -190,6 +195,8 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
GetMessageForRetry: func(requester, to types.JID, id types.MessageID) *waProto.Message { return nil },
appStateKeyRequests: make(map[string]time.Time),
pendingPhoneRerequests: make(map[types.MessageID]context.CancelFunc),
EnableAutoReconnect: true,
AutoTrustIdentity: true,
DontSendSelfBroadcast: true,
@@ -547,17 +554,46 @@ func (cli *Client) handleFrame(data []byte) {
cli.handlerQueue <- node
}()
}
} else {
} else if node.Tag != "ack" {
cli.Log.Debugf("Didn't handle WhatsApp node %s", node.Tag)
}
}
func stopAndDrainTimer(timer *time.Timer) {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
func (cli *Client) handlerQueueLoop(ctx context.Context) {
timer := time.NewTimer(5 * time.Minute)
stopAndDrainTimer(timer)
cli.Log.Debugf("Starting handler queue loop")
for {
select {
case node := <-cli.handlerQueue:
cli.nodeHandlers[node.Tag](node)
doneChan := make(chan struct{}, 1)
go func() {
start := time.Now()
cli.nodeHandlers[node.Tag](node)
duration := time.Since(start)
doneChan <- struct{}{}
if duration > 5*time.Second {
cli.Log.Warnf("Node handling took %s for %s", duration, node.XMLString())
}
}()
timer.Reset(5 * time.Minute)
select {
case <-doneChan:
stopAndDrainTimer(timer)
case <-timer.C:
cli.Log.Warnf("Node handling is taking long for %s - continuing in background", node.XMLString())
}
case <-ctx.Done():
cli.Log.Debugf("Closing handler queue loop")
return
}
}
@@ -609,6 +645,13 @@ func (cli *Client) dispatchEvent(evt interface{}) {
// yourNormalEventHandler(evt)
// }
func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessageInfo) (*events.Message, error) {
var err error
if chatJID.IsEmpty() {
chatJID, err = types.ParseJID(webMsg.GetKey().GetRemoteJid())
if err != nil {
return nil, fmt.Errorf("no chat JID provided and failed to parse remote JID: %w", err)
}
}
info := types.MessageInfo{
MessageSource: types.MessageSource{
Chat: chatJID,
@@ -619,7 +662,6 @@ func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessage
PushName: webMsg.GetPushName(),
Timestamp: time.Unix(int64(webMsg.GetMessageTimestamp()), 0),
}
var err error
if info.IsFromMe {
info.Sender = cli.getOwnID().ToNonAD()
if info.Sender.IsEmpty() {
@@ -638,8 +680,9 @@ func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessage
return nil, fmt.Errorf("failed to parse sender of message %s: %v", info.ID, err)
}
evt := &events.Message{
RawMessage: webMsg.GetMessage(),
Info: info,
RawMessage: webMsg.GetMessage(),
SourceWebMsg: webMsg,
Info: info,
}
evt.UnwrapRaw()
return evt, nil

View File

@@ -11,6 +11,7 @@ import (
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
@@ -79,9 +80,17 @@ func (cli *Client) handleIB(node *waBinary.Node) {
func (cli *Client) handleConnectFailure(node *waBinary.Node) {
ag := node.AttrGetter()
reason := events.ConnectFailureReason(ag.Int("reason"))
// Let the auto-reconnect happen for 503s, for all other failures block it
if reason != events.ConnectFailureServiceUnavailable {
message := ag.OptionalString("message")
willAutoReconnect := true
switch {
default:
// By default, expect a disconnect (i.e. prevent auto-reconnect)
cli.expectDisconnect()
willAutoReconnect = false
case reason == events.ConnectFailureServiceUnavailable:
// Auto-reconnect for 503s
case reason == 500 && message == "biz vname fetch error":
// These happen for business accounts randomly, also auto-reconnect
}
if reason.IsLoggedOut() {
cli.Log.Infof("Got %s connect failure, sending LoggedOut event and deleting session", reason)
@@ -97,13 +106,13 @@ func (cli *Client) handleConnectFailure(node *waBinary.Node) {
Expire: time.Duration(ag.Int("expire")) * time.Second,
})
} else if reason == events.ConnectFailureClientOutdated {
cli.Log.Errorf("Client outdated (405) connect failure")
cli.Log.Errorf("Client outdated (405) connect failure (client version: %s)", store.GetWAVersion().String())
go cli.dispatchEvent(&events.ClientOutdated{})
} else if reason == events.ConnectFailureServiceUnavailable {
cli.Log.Warnf("Got 503 connect failure, assuming automatic reconnect will handle it")
} else if willAutoReconnect {
cli.Log.Warnf("Got %d/%s connect failure, assuming automatic reconnect will handle it", int(reason), message)
} else {
cli.Log.Warnf("Unknown connect failure: %s", node.XMLString())
go cli.dispatchEvent(&events.ConnectFailure{Reason: reason, Raw: node})
go cli.dispatchEvent(&events.ConnectFailure{Reason: reason, Message: message, Raw: node})
}
}

View File

@@ -12,8 +12,10 @@ import (
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
@@ -233,7 +235,7 @@ func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHas
func (cli *Client) downloadAndDecrypt(url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSha256, fileSha256 []byte) (data []byte, err error) {
iv, cipherKey, macKey, _ := getMediaKeys(mediaKey, appInfo)
var ciphertext, mac []byte
if ciphertext, mac, err = cli.downloadEncryptedMedia(url, fileEncSha256); err != nil {
if ciphertext, mac, err = cli.downloadEncryptedMediaWithRetries(url, fileEncSha256); err != nil {
} else if err = validateMedia(iv, ciphertext, macKey, mac); err != nil {
@@ -252,6 +254,23 @@ func getMediaKeys(mediaKey []byte, appInfo MediaType) (iv, cipherKey, macKey, re
return mediaKeyExpanded[:16], mediaKeyExpanded[16:48], mediaKeyExpanded[48:80], mediaKeyExpanded[80:]
}
func (cli *Client) downloadEncryptedMediaWithRetries(url string, checksum []byte) (file, mac []byte, err error) {
for retryNum := 0; retryNum < 5; retryNum++ {
file, mac, err = cli.downloadEncryptedMedia(url, checksum)
if err == nil {
return
}
netErr, ok := err.(net.Error)
if !ok {
// Not a network error, don't retry
return
}
cli.Log.Warnf("Failed to download media due to network error: %w, retrying...", netErr)
time.Sleep(time.Duration(retryNum+1) * time.Second)
}
return
}
func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
var req *http.Request
req, err = http.NewRequest(http.MethodGet, url, nil)
@@ -268,7 +287,9 @@ func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, ma
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
if resp.StatusCode == http.StatusForbidden {
err = ErrMediaDownloadFailedWith403
} else if resp.StatusCode == http.StatusNotFound {
err = ErrMediaDownloadFailedWith404
} else if resp.StatusCode == http.StatusGone {
err = ErrMediaDownloadFailedWith410

View File

@@ -29,6 +29,8 @@ var (
ErrNoPushName = errors.New("can't send presence without PushName set")
ErrNoPrivacyToken = errors.New("no privacy token stored")
ErrAppStateUpdate = errors.New("server returned error updating app state")
)
// Errors that happen while confirming device pairing
@@ -107,6 +109,7 @@ var (
// Some errors that Client.Download can return
var (
ErrMediaDownloadFailedWith403 = errors.New("download failed with status code 403")
ErrMediaDownloadFailedWith404 = errors.New("download failed with status code 404")
ErrMediaDownloadFailedWith410 = errors.New("download failed with status code 410")
ErrNoURLPresent = errors.New("no url present")

View File

@@ -57,7 +57,7 @@ func (cli *Client) CreateGroup(req ReqCreateGroup) (*types.GroupInfo, error) {
}
}
if req.CreateKey == "" {
req.CreateKey = GenerateMessageID()
req.CreateKey = cli.GenerateMessageID()
}
if req.IsParent {
if req.DefaultMembershipApprovalMode == "" {
@@ -99,7 +99,7 @@ func (cli *Client) CreateGroup(req ReqCreateGroup) (*types.GroupInfo, error) {
func (cli *Client) UnlinkGroup(parent, child types.JID) error {
_, err := cli.sendGroupIQ(context.TODO(), iqSet, parent, waBinary.Node{
Tag: "unlink",
Attrs: waBinary.Attrs{"unlink_type": types.GroupLinkChangeTypeSub},
Attrs: waBinary.Attrs{"unlink_type": string(types.GroupLinkChangeTypeSub)},
Content: []waBinary.Node{{
Tag: "group",
Attrs: waBinary.Attrs{"jid": child},
@@ -116,7 +116,7 @@ func (cli *Client) LinkGroup(parent, child types.JID) error {
Tag: "links",
Content: []waBinary.Node{{
Tag: "link",
Attrs: waBinary.Attrs{"link_type": types.GroupLinkChangeTypeSub},
Attrs: waBinary.Attrs{"link_type": string(types.GroupLinkChangeTypeSub)},
Content: []waBinary.Node{{
Tag: "group",
Attrs: waBinary.Attrs{"jid": child},
@@ -221,7 +221,7 @@ func (cli *Client) SetGroupName(jid types.JID, name string) error {
//
// The previousID and newID fields are optional. If the previous ID is not specified, this will
// automatically fetch the current group info to find the previous topic ID. If the new ID is not
// specified, one will be generated with GenerateMessageID().
// specified, one will be generated with Client.GenerateMessageID().
func (cli *Client) SetGroupTopic(jid types.JID, previousID, newID, topic string) error {
if previousID == "" {
oldInfo, err := cli.GetGroupInfo(jid)
@@ -231,7 +231,7 @@ func (cli *Client) SetGroupTopic(jid types.JID, previousID, newID, topic string)
previousID = oldInfo.TopicID
}
if newID == "" {
newID = GenerateMessageID()
newID = cli.GenerateMessageID()
}
attrs := waBinary.Attrs{
"id": newID,

View File

@@ -10,6 +10,7 @@ import (
"context"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
type DangerousInternalClient struct {
@@ -62,6 +63,6 @@ func (int *DangerousInternalClient) RequestAppStateKeys(ctx context.Context, key
int.c.requestAppStateKeys(ctx, keyIDs)
}
func (int *DangerousInternalClient) SendRetryReceipt(node *waBinary.Node, forceIncludeIdentity bool) {
int.c.sendRetryReceipt(node, forceIncludeIdentity)
func (int *DangerousInternalClient) SendRetryReceipt(node *waBinary.Node, info *types.MessageInfo, forceIncludeIdentity bool) {
int.c.sendRetryReceipt(node, info, forceIncludeIdentity)
}

View File

@@ -23,10 +23,13 @@ var (
KeepAliveIntervalMin = 20 * time.Second
// KeepAliveIntervalMax specifies the maximum interval for websocket keepalive pings.
KeepAliveIntervalMax = 30 * time.Second
// KeepAliveMaxFailTime specifies the maximum time to wait before forcing a reconnect if keepalives fail repeatedly.
KeepAliveMaxFailTime = 3 * time.Minute
)
func (cli *Client) keepAliveLoop(ctx context.Context) {
var lastSuccess time.Time
lastSuccess := time.Now()
var errorCount int
for {
interval := rand.Int63n(KeepAliveIntervalMax.Milliseconds()-KeepAliveIntervalMin.Milliseconds()) + KeepAliveIntervalMin.Milliseconds()
@@ -41,6 +44,11 @@ func (cli *Client) keepAliveLoop(ctx context.Context) {
ErrorCount: errorCount,
LastSuccess: lastSuccess,
})
if cli.EnableAutoReconnect && time.Since(lastSuccess) > KeepAliveMaxFailTime {
cli.Log.Debugf("Forcing reconnect due to keepalive failure")
cli.Disconnect()
go cli.autoReconnect()
}
} else {
if errorCount > 0 {
errorCount = 0

View File

@@ -7,7 +7,6 @@
package whatsmeow
import (
"crypto/rand"
"fmt"
"google.golang.org/protobuf/proto"
@@ -18,6 +17,7 @@ import (
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/gcmutil"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/randbytes"
)
func getMediaRetryKey(mediaKey []byte) (cipherKey []byte) {
@@ -34,11 +34,7 @@ func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphe
err = fmt.Errorf("failed to marshal payload: %w", err)
return
}
iv = make([]byte, 12)
_, err = rand.Read(iv)
if err != nil {
panic(err)
}
iv = randbytes.Make(12)
ciphertext, err = gcmutil.Encrypt(getMediaRetryKey(mediaKey), iv, plaintext, []byte(messageID))
return
}

View File

@@ -9,7 +9,6 @@ package whatsmeow
import (
"bytes"
"compress/zlib"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
@@ -31,6 +30,7 @@ import (
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/randbytes"
)
var pbSerializer = store.SignalProtobufSerializer
@@ -101,6 +101,7 @@ func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, er
info.Timestamp = ag.UnixTime("t")
info.PushName = ag.OptionalString("notify")
info.Category = ag.OptionalString("category")
info.Type = ag.OptionalString("type")
if !ag.OK() {
return nil, ag.Error()
}
@@ -125,7 +126,7 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
go cli.sendAck(node)
if len(node.GetChildrenByTag("unavailable")) > 0 && len(node.GetChildrenByTag("enc")) == 0 {
cli.Log.Warnf("Unavailable message %s from %s", info.ID, info.SourceString())
go cli.sendRetryReceipt(node, true)
go cli.sendRetryReceipt(node, info, true)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: true})
return
}
@@ -137,7 +138,8 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
if child.Tag != "enc" {
continue
}
encType, ok := child.Attrs["type"].(string)
ag := child.AttrGetter()
encType, ok := ag.GetString("type", false)
if !ok {
continue
}
@@ -155,8 +157,13 @@ 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)
isUnavailable := encType == "skmsg" && !containsDirectMsg && errors.Is(err, signalerror.ErrNoSenderKeyForUser)
go cli.sendRetryReceipt(node, isUnavailable)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: isUnavailable})
go cli.sendRetryReceipt(node, info, isUnavailable)
decryptFailMode, _ := child.Attrs["decrypt-fail"].(string)
cli.dispatchEvent(&events.UndecryptableMessage{
Info: *info,
IsUnavailable: isUnavailable,
DecryptFailMode: events.DecryptFailMode(decryptFailMode),
})
return
}
@@ -166,8 +173,12 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
cli.Log.Warnf("Error unmarshaling decrypted message from %s: %v", info.SourceString(), err)
continue
}
retryCount := ag.OptionalInt("count")
if retryCount > 0 {
cli.cancelDelayedRequestFromPhone(info.ID)
}
cli.handleDecryptedMessage(info, &msg)
cli.handleDecryptedMessage(info, &msg, retryCount)
handled = true
}
if handled {
@@ -246,6 +257,9 @@ func isValidPadding(plaintext []byte) bool {
}
func unpadMessage(plaintext []byte) ([]byte, error) {
if len(plaintext) == 0 {
return nil, fmt.Errorf("plaintext is empty")
}
if checkPadding && !isValidPadding(plaintext) {
return nil, fmt.Errorf("plaintext doesn't have expected padding")
}
@@ -253,11 +267,7 @@ func unpadMessage(plaintext []byte) ([]byte, error) {
}
func padMessage(plaintext []byte) []byte {
var pad [1]byte
_, err := rand.Read(pad[:])
if err != nil {
panic(err)
}
pad := randbytes.Make(1)
pad[0] &= 0xf
if pad[0] == 0 {
pad[0] = 0xf
@@ -357,6 +367,25 @@ func (cli *Client) handleAppStateSyncKeyShare(keys *waProto.AppStateSyncKeyShare
}
}
func (cli *Client) handlePlaceholderResendResponse(msg *waProto.PeerDataOperationRequestResponseMessage) {
reqID := msg.GetStanzaId()
parts := msg.GetPeerDataOperationResult()
cli.Log.Debugf("Handling response to placeholder resend request %s with %d items", reqID, len(parts))
for i, part := range parts {
var webMsg waProto.WebMessageInfo
if resp := part.GetPlaceholderMessageResendResponse(); resp == nil {
cli.Log.Warnf("Missing response in item #%d of response to %s", i+1, reqID)
} else if err := proto.Unmarshal(resp.GetWebMessageInfoBytes(), &webMsg); err != nil {
cli.Log.Warnf("Failed to unmarshal protobuf web message in item #%d of response to %s: %v", i+1, reqID, err)
} else if msgEvt, err := cli.ParseWebMessage(types.EmptyJID, &webMsg); err != nil {
cli.Log.Warnf("Failed to parse web message info in item #%d of response to %s: %v", i+1, reqID, err)
} else {
msgEvt.UnavailableRequestID = reqID
cli.dispatchEvent(msgEvt)
}
}
}
func (cli *Client) handleProtocolMessage(info *types.MessageInfo, msg *waProto.Message) {
protoMsg := msg.GetProtocolMessage()
@@ -368,6 +397,10 @@ func (cli *Client) handleProtocolMessage(info *types.MessageInfo, msg *waProto.M
go cli.sendProtocolMessageReceipt(info.ID, "hist_sync")
}
if protoMsg.GetPeerDataOperationRequestResponseMessage().GetPeerDataOperationRequestType() == waProto.PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND {
go cli.handlePlaceholderResendResponse(protoMsg.GetPeerDataOperationRequestResponseMessage())
}
if protoMsg.GetAppStateSyncKeyShare() != nil && info.IsFromMe {
go cli.handleAppStateSyncKeyShare(protoMsg.AppStateSyncKeyShare)
}
@@ -472,9 +505,9 @@ func (cli *Client) storeHistoricalMessageSecrets(conversations []*waProto.Conver
}
}
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message) {
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message, retryCount int) {
cli.processProtocolParts(info, msg)
evt := &events.Message{Info: *info, RawMessage: msg}
evt := &events.Message{Info: *info, RawMessage: msg, RetryCount: retryCount}
cli.dispatchEvent(evt.UnwrapRaw())
}

View File

@@ -7,7 +7,6 @@
package whatsmeow
import (
"crypto/rand"
"crypto/sha256"
"fmt"
"time"
@@ -19,6 +18,7 @@ import (
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/gcmutil"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/randbytes"
)
type MsgSecretType string
@@ -107,11 +107,7 @@ func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.
}
secretKey, additionalData := generateMsgSecretKey(useCase, ownID, origMsgID, origSender, baseEncKey)
iv = make([]byte, 12)
_, err = rand.Read(iv)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate iv: %w", err)
}
iv = randbytes.Make(12)
ciphertext, err = gcmutil.Encrypt(secretKey, iv, plaintext, additionalData)
if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt secret message: %w", err)
@@ -132,7 +128,7 @@ func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.
// }
func (cli *Client) DecryptReaction(reaction *events.Message) (*waProto.ReactionMessage, error) {
encReaction := reaction.Message.GetEncReactionMessage()
if encReaction != nil {
if encReaction == nil {
return nil, ErrNotEncryptedReactionMessage
}
plaintext, err := cli.decryptMsgSecret(reaction, EncSecretReaction, encReaction, encReaction.GetTargetMessageKey())
@@ -225,11 +221,7 @@ func (cli *Client) BuildPollVote(pollInfo *types.MessageInfo, optionNames []stri
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildPollCreation("meow?", []string{"yes", "no"}, 1))
func (cli *Client) BuildPollCreation(name string, optionNames []string, selectableOptionCount int) *waProto.Message {
msgSecret := make([]byte, 32)
_, err := rand.Read(msgSecret)
if err != nil {
panic(err)
}
msgSecret := randbytes.Make(32)
if selectableOptionCount < 0 || selectableOptionCount > len(optionNames) {
selectableOptionCount = 0
}

View File

@@ -173,6 +173,11 @@ func (cli *Client) handleAccountSyncNotification(node *waBinary.Node) {
cli.handlePrivacySettingsNotification(&child)
case "devices":
cli.handleOwnDevicesNotification(&child)
case "picture":
cli.dispatchEvent(&events.Picture{
Timestamp: node.AttrGetter().UnixTime("t"),
JID: cli.getOwnID().ToNonAD(),
})
default:
cli.Log.Debugf("Unhandled account sync item %s", child.Tag)
}
@@ -254,6 +259,8 @@ func (cli *Client) handleNotification(node *waBinary.Node) {
go cli.handleMediaRetryNotification(node)
case "privacy_token":
go cli.handlePrivacyTokenNotification(node)
case "link_code_companion_reg":
go cli.tryHandleCodePairNotification(node)
// Other types: business, disappearing_mode, server, status, pay, psa
default:
cli.Log.Debugf("Unhandled notification with type %s", notifType)

252
vendor/go.mau.fi/whatsmeow/pair-code.go vendored Normal file
View File

@@ -0,0 +1,252 @@
// Copyright (c) 2023 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 (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base32"
"fmt"
"regexp"
"strconv"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/pbkdf2"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/keys"
"go.mau.fi/whatsmeow/util/randbytes"
)
// PairClientType is the type of client to use with PairCode.
// The type is automatically filled based on store.DeviceProps.PlatformType (which is what QR login uses).
type PairClientType int
const (
PairClientUnknown PairClientType = iota
PairClientChrome
PairClientEdge
PairClientFirefox
PairClientIE
PairClientOpera
PairClientSafari
PairClientElectron
PairClientUWP
PairClientOtherWebClient
)
func platformTypeToPairClientType(platformType waProto.DeviceProps_PlatformType) PairClientType {
switch platformType {
case waProto.DeviceProps_CHROME:
return PairClientChrome
case waProto.DeviceProps_EDGE:
return PairClientEdge
case waProto.DeviceProps_FIREFOX:
return PairClientFirefox
case waProto.DeviceProps_IE:
return PairClientIE
case waProto.DeviceProps_OPERA:
return PairClientOpera
case waProto.DeviceProps_SAFARI:
return PairClientSafari
case waProto.DeviceProps_DESKTOP:
return PairClientElectron
case waProto.DeviceProps_UWP:
return PairClientUWP
default:
return PairClientOtherWebClient
}
}
var notNumbers = regexp.MustCompile("[^0-9]")
var linkingBase32 = base32.NewEncoding("123456789ABCDEFGHJKLMNPQRSTVWXYZ")
type phoneLinkingCache struct {
jid types.JID
keyPair *keys.KeyPair
linkingCode string
pairingRef string
}
func generateCompanionEphemeralKey() (ephemeralKeyPair *keys.KeyPair, ephemeralKey []byte, encodedLinkingCode string) {
ephemeralKeyPair = keys.NewKeyPair()
salt := randbytes.Make(32)
iv := randbytes.Make(16)
linkingCode := randbytes.Make(5)
encodedLinkingCode = linkingBase32.EncodeToString(linkingCode)
linkCodeKey := pbkdf2.Key([]byte(encodedLinkingCode), salt, 2<<16, 32, sha256.New)
linkCipherBlock, _ := aes.NewCipher(linkCodeKey)
encryptedPubkey := ephemeralKeyPair.Pub[:]
cipher.NewCTR(linkCipherBlock, iv).XORKeyStream(encryptedPubkey, encryptedPubkey)
ephemeralKey = make([]byte, 80)
copy(ephemeralKey[0:32], salt)
copy(ephemeralKey[32:48], iv)
copy(ephemeralKey[48:80], encryptedPubkey)
return
}
// PairPhone generates a pairing code that can be used to link to a phone without scanning a QR code.
//
// The exact expiry of pairing codes is unknown, but QR codes are always generated and the login websocket is closed
// after the QR codes run out, which means there's a 160-second time limit. It is recommended to generate the pairing
// code immediately after connecting to the websocket to have the maximum time.
//
// See https://faq.whatsapp.com/1324084875126592 for more info
func (cli *Client) PairPhone(phone string, showPushNotification bool) (string, error) {
clientType := platformTypeToPairClientType(store.DeviceProps.GetPlatformType())
clientDisplayName := store.DeviceProps.GetOs()
ephemeralKeyPair, ephemeralKey, encodedLinkingCode := generateCompanionEphemeralKey()
phone = notNumbers.ReplaceAllString(phone, "")
jid := types.NewJID(phone, types.DefaultUserServer)
resp, err := cli.sendIQ(infoQuery{
Namespace: "md",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "link_code_companion_reg",
Attrs: waBinary.Attrs{
"jid": jid,
"stage": "companion_hello",
"should_show_push_notification": strconv.FormatBool(showPushNotification),
},
Content: []waBinary.Node{
{Tag: "link_code_pairing_wrapped_companion_ephemeral_pub", Content: ephemeralKey},
{Tag: "companion_server_auth_key_pub", Content: cli.Store.NoiseKey.Pub[:]},
{Tag: "companion_platform_id", Content: strconv.Itoa(int(clientType))},
{Tag: "companion_platform_display", Content: clientDisplayName},
{Tag: "link_code_pairing_nonce", Content: []byte{0}},
},
}},
})
if err != nil {
return "", err
}
pairingRefNode, ok := resp.GetOptionalChildByTag("link_code_companion_reg", "link_code_pairing_ref")
if !ok {
return "", &ElementMissingError{Tag: "link_code_pairing_ref", In: "code link registration response"}
}
pairingRef, ok := pairingRefNode.Content.([]byte)
if !ok {
return "", fmt.Errorf("unexpected type %T in content of link_code_pairing_ref tag", pairingRefNode.Content)
}
cli.phoneLinkingCache = &phoneLinkingCache{
jid: jid,
keyPair: ephemeralKeyPair,
linkingCode: encodedLinkingCode,
pairingRef: string(pairingRef),
}
return encodedLinkingCode[0:4] + "-" + encodedLinkingCode[4:], nil
}
func (cli *Client) tryHandleCodePairNotification(parentNode *waBinary.Node) {
err := cli.handleCodePairNotification(parentNode)
if err != nil {
cli.Log.Errorf("Failed to handle code pair notification: %s", err)
}
}
func (cli *Client) handleCodePairNotification(parentNode *waBinary.Node) error {
node, ok := parentNode.GetOptionalChildByTag("link_code_companion_reg")
if !ok {
return &ElementMissingError{
Tag: "link_code_companion_reg",
In: "notification",
}
}
linkCache := cli.phoneLinkingCache
if linkCache == nil {
return fmt.Errorf("received code pair notification without a pending pairing")
}
linkCodePairingRef, _ := node.GetChildByTag("link_code_pairing_ref").Content.([]byte)
if string(linkCodePairingRef) != linkCache.pairingRef {
return fmt.Errorf("pairing ref mismatch in code pair notification")
}
wrappedPrimaryEphemeralPub, ok := node.GetChildByTag("link_code_pairing_wrapped_primary_ephemeral_pub").Content.([]byte)
if !ok {
return &ElementMissingError{
Tag: "link_code_pairing_wrapped_primary_ephemeral_pub",
In: "notification",
}
}
primaryIdentityPub, ok := node.GetChildByTag("primary_identity_pub").Content.([]byte)
if !ok {
return &ElementMissingError{
Tag: "primary_identity_pub",
In: "notification",
}
}
advSecretRandom := randbytes.Make(32)
keyBundleSalt := randbytes.Make(32)
keyBundleNonce := randbytes.Make(12)
// Decrypt the primary device's ephemeral public key, which was encrypted with the 8-character pairing code,
// then compute the DH shared secret using our ephemeral private key we generated earlier.
primarySalt := wrappedPrimaryEphemeralPub[0:32]
primaryIV := wrappedPrimaryEphemeralPub[32:48]
primaryEncryptedPubkey := wrappedPrimaryEphemeralPub[48:80]
linkCodeKey := pbkdf2.Key([]byte(linkCache.linkingCode), primarySalt, 2<<16, 32, sha256.New)
linkCipherBlock, err := aes.NewCipher(linkCodeKey)
if err != nil {
return fmt.Errorf("failed to create link cipher: %w", err)
}
primaryDecryptedPubkey := make([]byte, 32)
cipher.NewCTR(linkCipherBlock, primaryIV).XORKeyStream(primaryDecryptedPubkey, primaryEncryptedPubkey)
ephemeralSharedSecret, err := curve25519.X25519(linkCache.keyPair.Priv[:], primaryDecryptedPubkey)
if err != nil {
return fmt.Errorf("failed to compute ephemeral shared secret: %w", err)
}
// Encrypt and wrap key bundle containing our identity key, the primary device's identity key and the randomness used for the adv key.
keyBundleEncryptionKey := hkdfutil.SHA256(ephemeralSharedSecret, keyBundleSalt, []byte("link_code_pairing_key_bundle_encryption_key"), 32)
keyBundleCipherBlock, err := aes.NewCipher(keyBundleEncryptionKey)
if err != nil {
return fmt.Errorf("failed to create key bundle cipher: %w", err)
}
keyBundleGCM, err := cipher.NewGCM(keyBundleCipherBlock)
if err != nil {
return fmt.Errorf("failed to create key bundle GCM: %w", err)
}
plaintextKeyBundle := concatBytes(cli.Store.IdentityKey.Pub[:], primaryIdentityPub, advSecretRandom)
encryptedKeyBundle := keyBundleGCM.Seal(nil, keyBundleNonce, plaintextKeyBundle, nil)
wrappedKeyBundle := concatBytes(keyBundleSalt, keyBundleNonce, encryptedKeyBundle)
// Compute the adv secret key (which is used to authenticate the pair-success event later)
identitySharedKey, err := curve25519.X25519(cli.Store.IdentityKey.Priv[:], primaryIdentityPub)
if err != nil {
return fmt.Errorf("failed to compute identity shared key: %w", err)
}
advSecretInput := append(append(ephemeralSharedSecret, identitySharedKey...), advSecretRandom...)
advSecret := hkdfutil.SHA256(advSecretInput, nil, []byte("adv_secret"), 32)
cli.Store.AdvSecretKey = advSecret
_, err = cli.sendIQ(infoQuery{
Namespace: "md",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "link_code_companion_reg",
Attrs: waBinary.Attrs{
"jid": linkCache.jid,
"stage": "companion_finish",
},
Content: []waBinary.Node{
{Tag: "link_code_pairing_wrapped_key_bundle", Content: wrappedKeyBundle},
{Tag: "companion_identity_public", Content: cli.Store.IdentityKey.Pub[:]},
{Tag: "link_code_pairing_ref", Content: linkCodePairingRef},
},
}},
})
return err
}

View File

@@ -124,6 +124,9 @@ func (cli *Client) sendAck(node *waBinary.Node) {
//
// The first JID parameter (chat) must always be set to the chat ID (user ID in DMs and group ID in group chats).
// The second JID parameter (sender) must be set in group chats and must be the user ID who sent the message.
//
// You can mark multiple messages as read at the same time, but only if the messages were sent by the same user.
// To mark messages by different users as read, you must call MarkRead multiple times (once for each user).
func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, sender types.JID) error {
node := waBinary.Node{
Tag: "receipt",

View File

@@ -169,7 +169,11 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", senderAD, len(keys))
}
}
encrypted, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle)
encAttrs := waBinary.Attrs{}
if mediaType := getMediaTypeFromMessage(msg); mediaType != "" {
encAttrs["mediatype"] = mediaType
}
encrypted, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle, encAttrs)
if err != nil {
return fmt.Errorf("failed to encrypt message for retry: %w", err)
}
@@ -193,14 +197,10 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if edit, ok := node.Attrs["edit"]; ok {
attrs["edit"] = edit
}
content := []waBinary.Node{*encrypted}
if includeDeviceIdentity {
content = append(content, cli.makeDeviceIdentityNode())
}
err = cli.sendNode(waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: content,
Content: cli.getMessageContent(*encrypted, msg, attrs, includeDeviceIdentity),
})
if err != nil {
return fmt.Errorf("failed to send retry message: %w", err)
@@ -209,8 +209,63 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
return nil
}
func (cli *Client) cancelDelayedRequestFromPhone(msgID types.MessageID) {
if !cli.AutomaticMessageRerequestFromPhone {
return
}
cli.pendingPhoneRerequestsLock.RLock()
cancelPendingRequest, ok := cli.pendingPhoneRerequests[msgID]
if ok {
cancelPendingRequest()
}
cli.pendingPhoneRerequestsLock.RUnlock()
}
// RequestFromPhoneDelay specifies how long to wait for the sender to resend the message before requesting from your phone.
// This is only used if Client.AutomaticMessageRerequestFromPhone is true.
var RequestFromPhoneDelay = 5 * time.Second
func (cli *Client) delayedRequestMessageFromPhone(info *types.MessageInfo) {
if !cli.AutomaticMessageRerequestFromPhone {
return
}
cli.pendingPhoneRerequestsLock.Lock()
_, alreadyRequesting := cli.pendingPhoneRerequests[info.ID]
if alreadyRequesting {
cli.pendingPhoneRerequestsLock.Unlock()
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cli.pendingPhoneRerequests[info.ID] = cancel
cli.pendingPhoneRerequestsLock.Unlock()
defer func() {
cli.pendingPhoneRerequestsLock.Lock()
delete(cli.pendingPhoneRerequests, info.ID)
cli.pendingPhoneRerequestsLock.Unlock()
}()
select {
case <-time.After(RequestFromPhoneDelay):
case <-ctx.Done():
cli.Log.Debugf("Cancelled delayed request for message %s from phone", info.ID)
return
}
_, err := cli.SendMessage(
ctx,
cli.Store.ID.ToNonAD(),
cli.BuildUnavailableMessageRequest(info.Chat, info.Sender, info.ID),
SendRequestExtra{Peer: true},
)
if err != nil {
cli.Log.Warnf("Failed to send request for unavailable message %s to phone: %v", info.ID, err)
} else {
cli.Log.Debugf("Requested message %s from phone", info.ID)
}
}
// sendRetryReceipt sends a retry receipt for an incoming message.
func (cli *Client) sendRetryReceipt(node *waBinary.Node, forceIncludeIdentity bool) {
func (cli *Client) sendRetryReceipt(node *waBinary.Node, info *types.MessageInfo, forceIncludeIdentity bool) {
id, _ := node.Attrs["id"].(string)
children := node.GetChildren()
var retryCountInMsg int
@@ -231,6 +286,9 @@ func (cli *Client) sendRetryReceipt(node *waBinary.Node, forceIncludeIdentity bo
cli.Log.Warnf("Not sending any more retry receipts for %s", id)
return
}
if retryCount == 1 {
go cli.delayedRequestMessageFromPhone(info)
}
var registrationIDBytes [4]byte
binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID)

View File

@@ -8,9 +8,9 @@ package whatsmeow
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
@@ -30,20 +30,35 @@ import (
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/randbytes"
)
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
//
// msgID := cli.GenerateMessageID()
// cli.SendMessage(context.Background(), targetJID, &waProto.Message{...}, whatsmeow.SendRequestExtra{ID: msgID})
func (cli *Client) GenerateMessageID() types.MessageID {
data := make([]byte, 8, 8+20+16)
binary.BigEndian.PutUint64(data, uint64(time.Now().Unix()))
ownID := cli.getOwnID()
if !ownID.IsEmpty() {
data = append(data, []byte(ownID.User)...)
data = append(data, []byte("@c.us")...)
}
data = append(data, randbytes.Make(16)...)
hash := sha256.Sum256(data)
return "3EB0" + strings.ToUpper(hex.EncodeToString(hash[:9]))
}
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
//
// msgID := whatsmeow.GenerateMessageID()
// cli.SendMessage(context.Background(), targetJID, &waProto.Message{...}, whatsmeow.SendRequestExtra{ID: msgID})
//
// Deprecated: WhatsApp web has switched to using a hash of the current timestamp, user id and random bytes. Use Client.GenerateMessageID instead.
func GenerateMessageID() types.MessageID {
id := make([]byte, 8)
_, err := rand.Read(id)
if err != nil {
// Out of entropy
panic(err)
}
return "3EB0" + strings.ToUpper(hex.EncodeToString(id))
return "3EB0" + strings.ToUpper(hex.EncodeToString(randbytes.Make(8)))
}
type MessageDebugTimings struct {
@@ -132,7 +147,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
}
if len(req.ID) == 0 {
req.ID = GenerateMessageID()
req.ID = cli.GenerateMessageID()
}
resp.ID = req.ID
@@ -216,17 +231,9 @@ func (cli *Client) RevokeMessage(chat types.JID, id types.MessageID) (SendRespon
return cli.SendMessage(context.TODO(), chat, cli.BuildRevoke(chat, types.EmptyJID, id))
}
// BuildRevoke builds a message revocation message using the given variables.
// The built message can be sent normally using Client.SendMessage.
//
// To revoke your own messages, pass your JID or an empty JID as the second parameter (sender).
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, types.EmptyJID, originalMessageID)
//
// To revoke someone else's messages when you are group admin, pass the message sender's JID as the second parameter.
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, senderJID, originalMessageID)
func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waProto.Message {
// BuildMessageKey builds a MessageKey object, which is used to refer to previous messages
// for things such as replies, revocations and reactions.
func (cli *Client) BuildMessageKey(chat, sender types.JID, id types.MessageID) *waProto.MessageKey {
key := &waProto.MessageKey{
FromMe: proto.Bool(true),
Id: proto.String(id),
@@ -238,14 +245,90 @@ func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waPr
key.Participant = proto.String(sender.ToNonAD().String())
}
}
return key
}
// BuildRevoke builds a message revocation message using the given variables.
// The built message can be sent normally using Client.SendMessage.
//
// To revoke your own messages, pass your JID or an empty JID as the second parameter (sender).
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, types.EmptyJID, originalMessageID)
//
// To revoke someone else's messages when you are group admin, pass the message sender's JID as the second parameter.
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, senderJID, originalMessageID)
func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waProto.Message {
return &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_REVOKE.Enum(),
Key: key,
Key: cli.BuildMessageKey(chat, sender, id),
},
}
}
// BuildReaction builds a message reaction message using the given variables.
// The built message can be sent normally using Client.SendMessage.
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildReaction(chat, senderJID, targetMessageID, "🐈️")
func (cli *Client) BuildReaction(chat, sender types.JID, id types.MessageID, reaction string) *waProto.Message {
return &waProto.Message{
ReactionMessage: &waProto.ReactionMessage{
Key: cli.BuildMessageKey(chat, sender, id),
Text: proto.String(reaction),
SenderTimestampMs: proto.Int64(time.Now().UnixMilli()),
},
}
}
// BuildUnavailableMessageRequest builds a message to request the user's primary device to send
// the copy of a message that this client was unable to decrypt.
//
// The built message can be sent using Client.SendMessage, but you must pass whatsmeow.SendRequestExtra{Peer: true} as the last parameter.
// The full response will come as a ProtocolMessage with type `PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE`.
// The response events will also be dispatched as normal *events.Message's with UnavailableRequestID set to the request message ID.
func (cli *Client) BuildUnavailableMessageRequest(chat, sender types.JID, id string) *waProto.Message {
return &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE.Enum(),
PeerDataOperationRequestMessage: &waProto.PeerDataOperationRequestMessage{
PeerDataOperationRequestType: waProto.PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND.Enum(),
PlaceholderMessageResendRequest: []*waProto.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest{{
MessageKey: cli.BuildMessageKey(chat, sender, id),
}},
},
},
}
}
// BuildHistorySyncRequest builds a message to request additional history from the user's primary device.
//
// The built message can be sent using Client.SendMessage, but you must pass whatsmeow.SendRequestExtra{Peer: true} as the last parameter.
// The response will come as an *events.HistorySync with type `ON_DEMAND`.
//
// The response will contain to `count` messages immediately before the given message.
// The recommended number of messages to request at a time is 50.
func (cli *Client) BuildHistorySyncRequest(lastKnownMessageInfo *types.MessageInfo, count int) *waProto.Message {
return &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE.Enum(),
PeerDataOperationRequestMessage: &waProto.PeerDataOperationRequestMessage{
PeerDataOperationRequestType: waProto.PeerDataOperationRequestType_HISTORY_SYNC_ON_DEMAND.Enum(),
HistorySyncOnDemandRequest: &waProto.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest{
ChatJid: proto.String(lastKnownMessageInfo.Chat.String()),
OldestMsgId: proto.String(lastKnownMessageInfo.ID),
OldestMsgFromMe: proto.Bool(lastKnownMessageInfo.IsFromMe),
OnDemandMsgCount: proto.Int32(int32(count)),
OldestMsgTimestampMs: proto.Int64(lastKnownMessageInfo.Timestamp.UnixMilli()),
},
},
},
}
}
// EditWindow specifies how long a message can be edited for after it was sent.
const EditWindow = 20 * time.Minute
// BuildEdit builds a message edit message using the given variables.
// The built message can be sent normally using Client.SendMessage.
//
@@ -399,11 +482,15 @@ func (cli *Client) sendGroup(ctx context.Context, to, ownID types.JID, id types.
phash := participantListHashV2(allDevices)
node.Attrs["phash"] = phash
node.Content = append(node.GetChildren(), waBinary.Node{
skMsg := waBinary.Node{
Tag: "enc",
Content: ciphertext,
Attrs: waBinary.Attrs{"v": "2", "type": "skmsg"},
})
}
if mediaType := getMediaTypeFromMessage(message); mediaType != "" {
skMsg.Attrs["mediatype"] = mediaType
}
node.Content = append(node.GetChildren(), skMsg)
start = time.Now()
data, err := cli.sendNodeAndGetData(*node)
@@ -463,16 +550,109 @@ func getTypeFromMessage(msg *waProto.Message) string {
return "reaction"
case msg.PollCreationMessage != nil, msg.PollUpdateMessage != nil:
return "poll"
case getMediaTypeFromMessage(msg) != "":
return "media"
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 getMediaTypeFromMessage(msg *waProto.Message) string {
switch {
case msg.ViewOnceMessage != nil:
return getMediaTypeFromMessage(msg.ViewOnceMessage.Message)
case msg.ViewOnceMessageV2 != nil:
return getMediaTypeFromMessage(msg.ViewOnceMessageV2.Message)
case msg.EphemeralMessage != nil:
return getMediaTypeFromMessage(msg.EphemeralMessage.Message)
case msg.DocumentWithCaptionMessage != nil:
return getMediaTypeFromMessage(msg.DocumentWithCaptionMessage.Message)
case msg.ExtendedTextMessage != nil && msg.ExtendedTextMessage.Title != nil:
return "url"
case msg.ImageMessage != nil:
return "image"
case msg.StickerMessage != nil:
return "sticker"
case msg.DocumentMessage != nil:
return "document"
case msg.AudioMessage != nil:
if msg.AudioMessage.GetPtt() {
return "ptt"
} else {
return "audio"
}
case msg.VideoMessage != nil:
if msg.VideoMessage.GetGifPlayback() {
return "gif"
} else {
return "video"
}
case msg.ContactMessage != nil:
return "vcard"
case msg.ContactsArrayMessage != nil:
return "contact_array"
case msg.ListMessage != nil:
return "list"
case msg.ListResponseMessage != nil:
return "list_response"
case msg.ButtonsResponseMessage != nil:
return "buttons_response"
case msg.OrderMessage != nil:
return "order"
case msg.ProductMessage != nil:
return "product"
case msg.InteractiveResponseMessage != nil:
return "native_flow_response"
default:
return ""
}
}
func getButtonTypeFromMessage(msg *waProto.Message) string {
switch {
case msg.ViewOnceMessage != nil:
return getButtonTypeFromMessage(msg.ViewOnceMessage.Message)
case msg.ViewOnceMessageV2 != nil:
return getButtonTypeFromMessage(msg.ViewOnceMessageV2.Message)
case msg.EphemeralMessage != nil:
return getButtonTypeFromMessage(msg.EphemeralMessage.Message)
case msg.ButtonsMessage != nil:
return "buttons"
case msg.ButtonsResponseMessage != nil:
return "buttons_response"
case msg.ListMessage != nil:
return "list"
case msg.ListResponseMessage != nil:
return "list_response"
case msg.InteractiveResponseMessage != nil:
return "interactive_response"
default:
return ""
}
}
func getButtonAttributes(msg *waProto.Message) waBinary.Attrs {
switch {
case msg.ViewOnceMessage != nil:
return getButtonAttributes(msg.ViewOnceMessage.Message)
case msg.ViewOnceMessageV2 != nil:
return getButtonAttributes(msg.ViewOnceMessageV2.Message)
case msg.EphemeralMessage != nil:
return getButtonAttributes(msg.EphemeralMessage.Message)
case msg.TemplateMessage != nil:
return waBinary.Attrs{}
case msg.ListMessage != nil:
return waBinary.Attrs{
"v": "2",
"type": strings.ToLower(waProto.ListMessage_ListType_name[int32(msg.ListMessage.GetListType())]),
}
default:
return waBinary.Attrs{}
}
}
const (
EditAttributeEmpty = ""
EditAttributeMessageEdit = "1"
@@ -484,6 +664,8 @@ const RemoveReactionText = ""
func getEditAttribute(msg *waProto.Message) string {
switch {
case msg.EditedMessage != nil && msg.EditedMessage.Message != nil:
return getEditAttribute(msg.EditedMessage.Message)
case msg.ProtocolMessage != nil && msg.ProtocolMessage.GetKey() != nil:
switch msg.ProtocolMessage.GetType() {
case waProto.ProtocolMessage_REVOKE:
@@ -493,7 +675,7 @@ func getEditAttribute(msg *waProto.Message) string {
return EditAttributeAdminRevoke
}
case waProto.ProtocolMessage_MESSAGE_EDIT:
if msg.EditedMessage != nil {
if msg.ProtocolMessage.EditedMessage != nil {
return EditAttributeMessageEdit
}
}
@@ -523,7 +705,7 @@ func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, mess
return nil, err
}
start = time.Now()
encrypted, isPreKey, err := cli.encryptMessageForDevice(plaintext, to, nil)
encrypted, isPreKey, err := cli.encryptMessageForDevice(plaintext, to, nil, nil)
timings.PeerEncrypt = time.Since(start)
if err != nil {
return nil, fmt.Errorf("failed to encrypt peer message for %s: %v", to, err)
@@ -539,34 +721,12 @@ func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, mess
}, nil
}
func (cli *Client) prepareMessageNode(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, participants []types.JID, plaintext, dsmPlaintext []byte, timings *MessageDebugTimings) (*waBinary.Node, []types.JID, error) {
start := time.Now()
allDevices, err := cli.GetUserDevicesContext(ctx, participants)
timings.GetDevices = time.Since(start)
if err != nil {
return nil, nil, fmt.Errorf("failed to get device list: %w", err)
}
attrs := waBinary.Attrs{
"id": id,
"type": getTypeFromMessage(message),
"to": to,
}
if editAttr := getEditAttribute(message); editAttr != "" {
attrs["edit"] = editAttr
}
start = time.Now()
participantNodes, includeIdentity := cli.encryptMessageForDevices(ctx, allDevices, ownID, id, plaintext, dsmPlaintext)
timings.PeerEncrypt = time.Since(start)
content := []waBinary.Node{{
Tag: "participants",
Content: participantNodes,
}}
func (cli *Client) getMessageContent(baseNode waBinary.Node, message *waProto.Message, msgAttrs waBinary.Attrs, includeIdentity bool) []waBinary.Node {
content := []waBinary.Node{baseNode}
if includeIdentity {
content = append(content, cli.makeDeviceIdentityNode())
}
if attrs["type"] == "poll" {
if msgAttrs["type"] == "poll" {
pollType := "creation"
if message.PollUpdateMessage != nil {
pollType = "vote"
@@ -578,10 +738,56 @@ func (cli *Client) prepareMessageNode(ctx context.Context, to, ownID types.JID,
},
})
}
if buttonType := getButtonTypeFromMessage(message); buttonType != "" {
content = append(content, waBinary.Node{
Tag: "biz",
Content: []waBinary.Node{{
Tag: buttonType,
Attrs: getButtonAttributes(message),
}},
})
}
return content
}
func (cli *Client) prepareMessageNode(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, participants []types.JID, plaintext, dsmPlaintext []byte, timings *MessageDebugTimings) (*waBinary.Node, []types.JID, error) {
start := time.Now()
allDevices, err := cli.GetUserDevicesContext(ctx, participants)
timings.GetDevices = time.Since(start)
if err != nil {
return nil, nil, fmt.Errorf("failed to get device list: %w", err)
}
msgType := getTypeFromMessage(message)
encAttrs := waBinary.Attrs{}
// Only include encMediaType for 1:1 messages (groups don't have a device-sent message plaintext)
if encMediaType := getMediaTypeFromMessage(message); dsmPlaintext != nil && encMediaType != "" {
encAttrs["mediatype"] = encMediaType
}
attrs := waBinary.Attrs{
"id": id,
"type": msgType,
"to": to,
}
if editAttr := getEditAttribute(message); editAttr != "" {
attrs["edit"] = editAttr
encAttrs["decrypt-fail"] = string(events.DecryptFailHide)
}
if msgType == "reaction" {
encAttrs["decrypt-fail"] = string(events.DecryptFailHide)
}
start = time.Now()
participantNodes, includeIdentity := cli.encryptMessageForDevices(ctx, allDevices, ownID, id, plaintext, dsmPlaintext, encAttrs)
timings.PeerEncrypt = time.Since(start)
participantNode := waBinary.Node{
Tag: "participants",
Content: participantNodes,
}
return &waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: content,
Content: cli.getMessageContent(participantNode, message, attrs, includeIdentity),
}, allDevices, nil
}
@@ -619,7 +825,7 @@ func (cli *Client) makeDeviceIdentityNode() waBinary.Node {
}
}
func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []types.JID, ownID types.JID, id string, msgPlaintext, dsmPlaintext []byte) ([]waBinary.Node, bool) {
func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []types.JID, ownID types.JID, id string, msgPlaintext, dsmPlaintext []byte, encAttrs waBinary.Attrs) ([]waBinary.Node, bool) {
includeIdentity := false
participantNodes := make([]waBinary.Node, 0, len(allDevices))
var retryDevices []types.JID
@@ -631,7 +837,7 @@ func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []ty
}
plaintext = dsmPlaintext
}
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, nil)
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, nil, encAttrs)
if errors.Is(err, ErrNoSession) {
retryDevices = append(retryDevices, jid)
continue
@@ -659,7 +865,7 @@ func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []ty
if jid.User == ownID.User && dsmPlaintext != nil {
plaintext = dsmPlaintext
}
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, resp.bundle)
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, resp.bundle, encAttrs)
if err != nil {
cli.Log.Warnf("Failed to encrypt %s for %s (retry): %v", id, jid, err)
continue
@@ -674,8 +880,8 @@ func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []ty
return participantNodes, includeIdentity
}
func (cli *Client) encryptMessageForDeviceAndWrap(plaintext []byte, to types.JID, bundle *prekey.Bundle) (*waBinary.Node, bool, error) {
node, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, to, bundle)
func (cli *Client) encryptMessageForDeviceAndWrap(plaintext []byte, to types.JID, bundle *prekey.Bundle, encAttrs waBinary.Attrs) (*waBinary.Node, bool, error) {
node, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, to, bundle, encAttrs)
if err != nil {
return nil, false, err
}
@@ -686,7 +892,13 @@ func (cli *Client) encryptMessageForDeviceAndWrap(plaintext []byte, to types.JID
}, includeDeviceIdentity, nil
}
func (cli *Client) encryptMessageForDevice(plaintext []byte, to types.JID, bundle *prekey.Bundle) (*waBinary.Node, bool, error) {
func copyAttrs(from, to waBinary.Attrs) {
for k, v := range from {
to[k] = v
}
}
func (cli *Client) encryptMessageForDevice(plaintext []byte, to types.JID, bundle *prekey.Bundle, extraAttrs waBinary.Attrs) (*waBinary.Node, bool, error) {
builder := session.NewBuilderFromSignal(cli.Store, to.SignalAddress(), pbSerializer)
if bundle != nil {
cli.Log.Debugf("Processing prekey bundle for %s", to)
@@ -708,17 +920,18 @@ func (cli *Client) encryptMessageForDevice(plaintext []byte, to types.JID, bundl
return nil, false, fmt.Errorf("cipher encryption failed: %w", err)
}
encType := "msg"
if ciphertext.Type() == protocol.PREKEY_TYPE {
encType = "pkmsg"
encAttrs := waBinary.Attrs{
"v": "2",
"type": "msg",
}
if ciphertext.Type() == protocol.PREKEY_TYPE {
encAttrs["type"] = "pkmsg"
}
copyAttrs(extraAttrs, encAttrs)
return &waBinary.Node{
Tag: "enc",
Attrs: waBinary.Attrs{
"v": "2",
"type": encType,
},
Tag: "enc",
Attrs: encAttrs,
Content: ciphertext.Serialize(),
}, encType == "pkmsg", nil
}, encAttrs["type"] == "pkmsg", nil
}

View File

@@ -74,7 +74,7 @@ func (vc WAVersionContainer) ProtoAppVersion() *waProto.ClientPayload_UserAgent_
}
// waVersion is the WhatsApp web client version
var waVersion = WAVersionContainer{2, 2310, 5}
var waVersion = WAVersionContainer{2, 2332, 15}
// waVersionHash is the md5 hash of a dot-separated waVersion
var waVersionHash [16]byte

View File

@@ -7,7 +7,6 @@
package sqlstore
import (
"crypto/rand"
"database/sql"
"errors"
"fmt"
@@ -18,6 +17,7 @@ import (
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
"go.mau.fi/whatsmeow/util/randbytes"
)
// Container is a wrapper for a SQL database that can contain multiple whatsmeow sessions.
@@ -65,7 +65,12 @@ func New(dialect, address string, log waLog.Logger) (*Container, error) {
// if err != nil {
// panic(err)
// }
// container, err := sqlstore.NewWithDB(db, "sqlite3", nil)
// container := sqlstore.NewWithDB(db, "sqlite3", nil)
//
// This method does not call Upgrade automatically like New does, so you must call it yourself:
//
// container := sqlstore.NewWithDB(...)
// err := container.Upgrade()
func NewWithDB(db *sql.DB, dialect string, log waLog.Logger) *Container {
if log == nil {
log = waLog.Noop
@@ -205,11 +210,7 @@ func (c *Container) NewDevice() *store.Device {
NoiseKey: keys.NewKeyPair(),
IdentityKey: keys.NewKeyPair(),
RegistrationID: mathRand.Uint32(),
AdvSecretKey: make([]byte, 32),
}
_, err := rand.Read(device.AdvSecretKey)
if err != nil {
panic(err)
AdvSecretKey: randbytes.Make(32),
}
device.SignedPreKey = device.IdentityKey.CreateSignedPreKey(1)
return device

View File

@@ -284,7 +284,8 @@ const (
SET key_data=excluded.key_data, timestamp=excluded.timestamp, fingerprint=excluded.fingerprint
WHERE excluded.timestamp > whatsmeow_app_state_sync_keys.timestamp
`
getAppStateSyncKeyQuery = `SELECT key_data, timestamp, fingerprint FROM whatsmeow_app_state_sync_keys WHERE jid=$1 AND key_id=$2`
getAppStateSyncKeyQuery = `SELECT key_data, timestamp, fingerprint FROM whatsmeow_app_state_sync_keys WHERE jid=$1 AND key_id=$2`
getLatestAppStateSyncKeyIDQuery = `SELECT key_id FROM whatsmeow_app_state_sync_keys WHERE jid=$1 ORDER BY timestamp DESC LIMIT 1`
)
func (s *SQLStore) PutAppStateSyncKey(id []byte, key store.AppStateSyncKey) error {
@@ -301,6 +302,15 @@ func (s *SQLStore) GetAppStateSyncKey(id []byte) (*store.AppStateSyncKey, error)
return &key, err
}
func (s *SQLStore) GetLatestAppStateSyncKeyID() ([]byte, error) {
var keyID []byte
err := s.db.QueryRow(getLatestAppStateSyncKeyIDQuery, s.JID).Scan(&keyID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return keyID, err
}
const (
putAppStateVersionQuery = `
INSERT INTO whatsmeow_app_state_version (jid, name, version, hash) VALUES ($1, $2, $3, $4)

View File

@@ -55,6 +55,7 @@ type AppStateSyncKey struct {
type AppStateSyncKeyStore interface {
PutAppStateSyncKey(id []byte, key AppStateSyncKey) error
GetAppStateSyncKey(id []byte) (*AppStateSyncKey, error)
GetLatestAppStateSyncKeyID() ([]byte, error)
}
type AppStateMutationMAC struct {

View File

@@ -19,7 +19,8 @@ type Contact struct {
JID types.JID // The contact who was modified.
Timestamp time.Time // The time when the modification happened.'
Action *waProto.ContactAction // The new contact info.
Action *waProto.ContactAction // The new contact info.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// PushName is emitted when a message is received with a different push name than the previous value cached for the same user.
@@ -43,7 +44,8 @@ type Pin struct {
JID types.JID // The chat which was pinned or unpinned.
Timestamp time.Time // The time when the (un)pinning happened.
Action *waProto.PinAction // Whether the chat is now pinned or not.
Action *waProto.PinAction // Whether the chat is now pinned or not.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// Star is emitted when a message is starred or unstarred from another device.
@@ -54,7 +56,8 @@ type Star struct {
MessageID string // The message which was starred or unstarred.
Timestamp time.Time // The time when the (un)starring happened.
Action *waProto.StarAction // Whether the message is now starred or not.
Action *waProto.StarAction // Whether the message is now starred or not.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// DeleteForMe is emitted when a message is deleted (for the current user only) from another device.
@@ -65,7 +68,8 @@ type DeleteForMe struct {
MessageID string // The message which was deleted.
Timestamp time.Time // The time when the deletion happened.
Action *waProto.DeleteMessageForMeAction // Additional information for the deletion.
Action *waProto.DeleteMessageForMeAction // Additional information for the deletion.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// Mute is emitted when a chat is muted or unmuted from another device.
@@ -73,7 +77,8 @@ type Mute struct {
JID types.JID // The chat which was muted or unmuted.
Timestamp time.Time // The time when the (un)muting happened.
Action *waProto.MuteAction // The current mute status of the chat.
Action *waProto.MuteAction // The current mute status of the chat.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// Archive is emitted when a chat is archived or unarchived from another device.
@@ -81,7 +86,8 @@ type Archive struct {
JID types.JID // The chat which was archived or unarchived.
Timestamp time.Time // The time when the (un)archiving happened.
Action *waProto.ArchiveChatAction // The current archival status of the chat.
Action *waProto.ArchiveChatAction // The current archival status of the chat.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// MarkChatAsRead is emitted when a whole chat is marked as read or unread from another device.
@@ -89,7 +95,17 @@ type MarkChatAsRead struct {
JID types.JID // The chat which was marked as read or unread.
Timestamp time.Time // The time when the marking happened.
Action *waProto.MarkChatAsReadAction // Whether the chat was marked as read or unread, and info about the most recent messages.
Action *waProto.MarkChatAsReadAction // Whether the chat was marked as read or unread, and info about the most recent messages.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// ClearChat is emitted when a chat is cleared on another device. This is different from DeleteChat.
type ClearChat struct {
JID types.JID // The chat which was cleared.
Timestamp time.Time // The time when the clear happened.
Action *waProto.ClearChatAction // Information about the clear.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// DeleteChat is emitted when a chat is deleted on another device.
@@ -97,21 +113,33 @@ type DeleteChat struct {
JID types.JID // The chat which was deleted.
Timestamp time.Time // The time when the deletion happened.
Action *waProto.DeleteChatAction // Information about the deletion.
Action *waProto.DeleteChatAction // Information about the deletion.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// PushNameSetting is emitted when the user's push name is changed from another device.
type PushNameSetting struct {
Timestamp time.Time // The time when the push name was changed.
Action *waProto.PushNameSetting // The new push name for the user.
Action *waProto.PushNameSetting // The new push name for the user.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// UnarchiveChatsSetting is emitted when the user changes the "Keep chats archived" setting from another device.
type UnarchiveChatsSetting struct {
Timestamp time.Time // The time when the setting was changed.
Action *waProto.UnarchiveChatsSetting // The new settings.
Action *waProto.UnarchiveChatsSetting // The new settings.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// UserStatusMute is emitted when the user mutes or unmutes another user's status updates.
type UserStatusMute struct {
JID types.JID // The user who was muted or unmuted
Timestamp time.Time // The timestamp when the action happened
Action *waProto.UserStatusMuteAction // The new mute status
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// AppState is emitted directly for new data received from app state syncing.

View File

@@ -176,8 +176,9 @@ func (cfr ConnectFailureReason) String() string {
//
// Known reasons are handled internally and emitted as different events (e.g. LoggedOut and TemporaryBan).
type ConnectFailure struct {
Reason ConnectFailureReason
Raw *waBinary.Node
Reason ConnectFailureReason
Message string
Raw *waBinary.Node
}
// ClientOutdated is emitted when the WhatsApp server rejects the connection with the ConnectFailureClientOutdated code.
@@ -199,6 +200,13 @@ type HistorySync struct {
Data *waProto.HistorySync
}
type DecryptFailMode string
const (
DecryptFailShow DecryptFailMode = ""
DecryptFailHide DecryptFailMode = "hide"
)
// UndecryptableMessage is emitted when receiving a new message that failed to decrypt.
//
// The library will automatically ask the sender to retry. If the sender resends the message,
@@ -211,6 +219,8 @@ type UndecryptableMessage struct {
// IsUnavailable is true if the recipient device didn't send a ciphertext to this device at all
// (as opposed to sending a ciphertext, but the ciphertext not being decryptable).
IsUnavailable bool
DecryptFailMode DecryptFailMode
}
// Message is emitted when receiving a new message.
@@ -224,6 +234,13 @@ type Message struct {
IsDocumentWithCaption bool // True if the message was unwrapped from a DocumentWithCaptionMessage
IsEdit bool // True if the message was unwrapped from an EditedMessage
// If this event was parsed from a WebMessageInfo (i.e. from a history sync or unavailable message request), the source data is here.
SourceWebMsg *waProto.WebMessageInfo
// If this event is a response to an unavailable message request, the request ID is here.
UnavailableRequestID types.MessageID
// If the message was re-requested from the sender, this is the number of retries it took.
RetryCount int
// The raw message struct. This is the raw unmodified data, which means the actual message might
// be wrapped in DeviceSentMessage, EphemeralMessage or ViewOnceMessage.
RawMessage *waProto.Message

View File

@@ -10,7 +10,6 @@ import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
@@ -20,6 +19,7 @@ import (
"go.mau.fi/whatsmeow/socket"
"go.mau.fi/whatsmeow/util/cbcutil"
"go.mau.fi/whatsmeow/util/randbytes"
)
// UploadResponse contains the data from the attachment upload, which can be put into a message to send the attachment.
@@ -62,11 +62,7 @@ type UploadResponse struct {
// The same applies to the other message types like DocumentMessage, just replace the struct type and Message field name.
func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaType) (resp UploadResponse, err error) {
resp.FileLength = uint64(len(plaintext))
resp.MediaKey = make([]byte, 32)
_, err = rand.Read(resp.MediaKey)
if err != nil {
return
}
resp.MediaKey = randbytes.Make(32)
plaintextSHA256 := sha256.Sum256(plaintext)
resp.FileSHA256 = plaintextSHA256[:]

View File

@@ -8,11 +8,10 @@
package keys
import (
"crypto/rand"
"fmt"
"go.mau.fi/libsignal/ecc"
"golang.org/x/crypto/curve25519"
"go.mau.fi/whatsmeow/util/randbytes"
)
type KeyPair struct {
@@ -32,12 +31,7 @@ func NewKeyPairFromPrivateKey(priv [32]byte) *KeyPair {
}
func NewKeyPair() *KeyPair {
var priv [32]byte
_, err := rand.Read(priv[:])
if err != nil {
panic(fmt.Errorf("failed to generate curve25519 private key: %w", err))
}
priv := *(*[32]byte)(randbytes.Make(32))
priv[0] &= 248
priv[31] &= 127

View File

@@ -0,0 +1,15 @@
package randbytes
import (
"crypto/rand"
"fmt"
)
func Make(length int) []byte {
random := make([]byte, length)
_, err := rand.Read(random)
if err != nil {
panic(fmt.Errorf("failed to get random bytes: %w", err))
}
return random
}