Update dependencies (#1951)

This commit is contained in:
Wim
2023-01-28 22:57:53 +01:00
committed by GitHub
parent eac2a8c8dc
commit 880586bac4
325 changed files with 151452 additions and 141118 deletions
+3 -2
View File
@@ -270,11 +270,12 @@ func (cli *Client) requestAppStateKeys(ctx context.Context, rawKeyIDs [][]byte)
},
},
}
if cli.Store.ID == nil {
ownID := cli.getOwnID().ToNonAD()
if ownID.IsEmpty() {
return
}
cli.Log.Infof("Sending key request for app state keys %+v", debugKeyIDs)
_, err := cli.SendMessage(ctx, cli.Store.ID.ToNonAD(), "", msg)
_, err := cli.SendMessage(ctx, ownID, msg, SendRequestExtra{Peer: true})
if err != nil {
cli.Log.Warnf("Failed to send app state key request: %v", err)
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
+9
View File
@@ -807,6 +807,7 @@ message ContextInfo {
optional string parentGroupJid = 35;
optional string trustBannerType = 37;
optional uint32 trustBannerAction = 38;
optional bool isSampled = 39;
}
message ActionLink {
@@ -926,6 +927,7 @@ message Message {
optional EncReactionMessage encReactionMessage = 56;
optional FutureProofMessage editedMessage = 58;
optional FutureProofMessage viewOnceMessageV2Extension = 59;
optional PollCreationMessage pollCreationMessageV2 = 60;
}
message MessageContextInfo {
@@ -1360,6 +1362,7 @@ message MsgOpaqueData {
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;
@@ -1495,6 +1498,7 @@ message SyncActionValue {
optional RemoveRecentStickerAction removeRecentStickerAction = 34;
optional ChatAssignmentAction chatAssignment = 35;
optional ChatAssignmentOpenedStatusAction chatAssignmentOpenedStatus = 36;
optional PnForLidChatAction pnForLidChatAction = 37;
}
message UserStatusMuteAction {
@@ -1575,6 +1579,10 @@ message PrimaryFeature {
repeated string flags = 1;
}
message PnForLidChatAction {
optional string pnJid = 1;
}
message PinAction {
optional bool pinned = 1;
}
@@ -2249,6 +2257,7 @@ message PollUpdate {
optional MessageKey pollUpdateMessageKey = 1;
optional PollVoteMessage vote = 2;
optional int64 senderTimestampMs = 3;
optional int64 serverTimestampMs = 4;
}
message PollAdditionalMetadata {
+6 -2
View File
@@ -25,16 +25,20 @@ func (cli *Client) getBroadcastListParticipants(jid types.JID) ([]types.JID, err
if err != nil {
return nil, err
}
ownID := cli.getOwnID().ToNonAD()
if ownID.IsEmpty() {
return nil, ErrNotLoggedIn
}
var hasSelf bool
for _, participant := range list {
if participant.User == cli.Store.ID.User {
if participant.User == ownID.User {
hasSelf = true
break
}
}
if !hasSelf {
list = append(list, cli.Store.ID.ToNonAD())
list = append(list, ownID)
}
return list, nil
}
+22 -3
View File
@@ -114,11 +114,18 @@ type Client struct {
// If it returns false, the accepting will be cancelled and the retry receipt will be ignored.
PreRetryCallback func(receipt *events.Receipt, id types.MessageID, retryCount int, msg *waProto.Message) bool
// PrePairCallback is called before pairing is completed. If it returns false, the pairing will be cancelled and
// the client will disconnect.
PrePairCallback func(jid types.JID, platform, businessName string) bool
// Should untrusted identity errors be handled automatically? If true, the stored identity and existing signal
// sessions will be removed on untrusted identity errors, and an events.IdentityChange will be dispatched.
// If false, decrypting a message from untrusted devices will fail.
AutoTrustIdentity bool
// Should SubscribePresence return an error if no privacy token is stored for the user?
ErrorOnSubscribePresenceWithoutToken bool
uniqueID string
idCounter uint32
@@ -250,6 +257,14 @@ func (cli *Client) closeSocketWaitChan() {
cli.socketLock.Unlock()
}
func (cli *Client) getOwnID() types.JID {
id := cli.Store.ID
if id == nil {
return types.EmptyJID
}
return *id
}
func (cli *Client) WaitForConnection(timeout time.Duration) bool {
timeoutChan := time.After(timeout)
cli.socketLock.RLock()
@@ -392,7 +407,8 @@ func (cli *Client) unlockedDisconnect() {
// Note that this will not emit any events. The LoggedOut event is only used for external logouts
// (triggered by the user from the main device or by WhatsApp servers).
func (cli *Client) Logout() error {
if cli.Store.ID == nil {
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return ErrNotLoggedIn
}
_, err := cli.sendIQ(infoQuery{
@@ -402,7 +418,7 @@ func (cli *Client) Logout() error {
Content: []waBinary.Node{{
Tag: "remove-companion-device",
Attrs: waBinary.Attrs{
"jid": *cli.Store.ID,
"jid": ownID,
"reason": "user_initiated",
},
}},
@@ -600,7 +616,10 @@ func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessage
}
var err error
if info.IsFromMe {
info.Sender = cli.Store.ID.ToNonAD()
info.Sender = cli.getOwnID().ToNonAD()
if info.Sender.IsEmpty() {
return nil, ErrNotLoggedIn
}
} else if chatJID.Server == types.DefaultUserServer {
info.Sender = chatJID
} else if webMsg.GetParticipant() != "" {
+1 -2
View File
@@ -89,10 +89,9 @@ func (cli *Client) handleConnectFailure(node *waBinary.Node) {
}
} else if reason == events.ConnectFailureTempBanned {
cli.Log.Warnf("Temporary ban connect failure: %s", node.XMLString())
expiryTime := ag.UnixTime("expire")
go cli.dispatchEvent(&events.TemporaryBan{
Code: events.TempBanReason(ag.Int("code")),
Expire: expiryTime,
Expire: time.Duration(ag.Int("expire")) * time.Second,
})
} else if reason == events.ConnectFailureClientOutdated {
cli.Log.Errorf("Client outdated (405) connect failure")
+2
View File
@@ -70,6 +70,7 @@ var (
_ DownloadableMessage = (*waProto.VideoMessage)(nil)
_ DownloadableMessage = (*waProto.DocumentMessage)(nil)
_ DownloadableMessage = (*waProto.StickerMessage)(nil)
_ DownloadableMessage = (*waProto.StickerMetadata)(nil)
_ DownloadableMessage = (*waProto.HistorySyncNotification)(nil)
_ DownloadableMessage = (*waProto.ExternalBlobReference)(nil)
_ DownloadableThumbnail = (*waProto.ExtendedTextMessage)(nil)
@@ -96,6 +97,7 @@ var classToMediaType = map[protoreflect.Name]MediaType{
"VideoMessage": MediaVideo,
"DocumentMessage": MediaDocument,
"StickerMessage": MediaImage,
"StickerMetadata": MediaImage,
"HistorySyncNotification": MediaHistory,
"ExternalBlobReference": MediaAppState,
+50 -6
View File
@@ -27,8 +27,45 @@ var (
ErrQRStoreContainsID = errors.New("GetQRChannel can only be called when there's no user ID in the client's Store")
ErrNoPushName = errors.New("can't send presence without PushName set")
ErrNoPrivacyToken = errors.New("no privacy token stored")
)
// Errors that happen while confirming device pairing
var (
ErrPairInvalidDeviceIdentityHMAC = errors.New("invalid device identity HMAC in pair success message")
ErrPairInvalidDeviceSignature = errors.New("invalid device signature in pair success message")
ErrPairRejectedLocally = errors.New("local PrePairCallback rejected pairing")
)
// PairProtoError is included in an events.PairError if the pairing failed due to a protobuf error.
type PairProtoError struct {
Message string
ProtoErr error
}
func (err *PairProtoError) Error() string {
return fmt.Sprintf("%s: %v", err.Message, err.ProtoErr)
}
func (err *PairProtoError) Unwrap() error {
return err.ProtoErr
}
// PairDatabaseError is included in an events.PairError if the pairing failed due to being unable to save the credentials to the device store.
type PairDatabaseError struct {
Message string
DBErr error
}
func (err *PairDatabaseError) Error() string {
return fmt.Sprintf("%s: %v", err.Message, err.DBErr)
}
func (err *PairDatabaseError) Unwrap() error {
return err.DBErr
}
var (
// ErrProfilePictureUnauthorized is returned by GetProfilePictureInfo when trying to get the profile picture of a user
// whose privacy settings prevent you from seeing their profile picture (status code 401).
@@ -65,6 +102,7 @@ var (
ErrBroadcastListUnsupported = errors.New("sending to non-status broadcast lists is not yet supported")
ErrUnknownServer = errors.New("can't send message to unknown server")
ErrRecipientADJID = errors.New("message recipient must be normal (non-AD) JID")
ErrServerReturnedError = errors.New("server returned error")
)
// Some errors that Client.Download can return
@@ -118,12 +156,18 @@ type IQError struct {
// Common errors returned by info queries for use with errors.Is
var (
ErrIQBadRequest error = &IQError{Code: 400, Text: "bad-request"}
ErrIQNotAuthorized error = &IQError{Code: 401, Text: "not-authorized"}
ErrIQForbidden error = &IQError{Code: 403, Text: "forbidden"}
ErrIQNotFound error = &IQError{Code: 404, Text: "item-not-found"}
ErrIQNotAcceptable error = &IQError{Code: 406, Text: "not-acceptable"}
ErrIQGone error = &IQError{Code: 410, Text: "gone"}
ErrIQBadRequest error = &IQError{Code: 400, Text: "bad-request"}
ErrIQNotAuthorized error = &IQError{Code: 401, Text: "not-authorized"}
ErrIQForbidden error = &IQError{Code: 403, Text: "forbidden"}
ErrIQNotFound error = &IQError{Code: 404, Text: "item-not-found"}
ErrIQNotAllowed error = &IQError{Code: 405, Text: "not-allowed"}
ErrIQNotAcceptable error = &IQError{Code: 406, Text: "not-acceptable"}
ErrIQGone error = &IQError{Code: 410, Text: "gone"}
ErrIQResourceLimit error = &IQError{Code: 419, Text: "resource-limit"}
ErrIQLocked error = &IQError{Code: 423, Text: "locked"}
ErrIQInternalServerError error = &IQError{Code: 500, Text: "internal-server-error"}
ErrIQServiceUnavailable error = &IQError{Code: 503, Text: "service-unavailable"}
ErrIQPartialServerError error = &IQError{Code: 530, Text: "partial-server-error"}
)
func parseIQError(node *waBinary.Node) error {
+172 -13
View File
@@ -29,31 +29,58 @@ func (cli *Client) sendGroupIQ(ctx context.Context, iqType infoQueryType, jid ty
})
}
// ReqCreateGroup contains the request data for CreateGroup.
type ReqCreateGroup struct {
// Group names are limited to 25 characters. A longer group name will cause a 406 not acceptable error.
Name string
// You don't need to include your own JID in the participants array, the WhatsApp servers will add it implicitly.
Participants []types.JID
// A create key can be provided to deduplicate the group create notification that will be triggered
// when the group is created. If provided, the JoinedGroup event will contain the same key.
CreateKey types.MessageID
// Set IsParent to true to create a community instead of a normal group.
// When creating a community, the linked announcement group will be created automatically by the server.
types.GroupParent
// Set LinkedParentJID to create a group inside a community.
types.GroupLinkedParent
}
// CreateGroup creates a group on WhatsApp with the given name and participants.
//
// You don't need to include your own JID in the participants array, the WhatsApp servers will add it implicitly.
//
// Group names are limited to 25 characters. A longer group name will cause a 406 not acceptable error.
//
// Optionally, a create key can be provided to deduplicate the group create notification that will be triggered
// when the group is created. If provided, the JoinedGroup event will contain the same key.
func (cli *Client) CreateGroup(name string, participants []types.JID, createKey types.MessageID) (*types.GroupInfo, error) {
participantNodes := make([]waBinary.Node, len(participants))
for i, participant := range participants {
// See ReqCreateGroup for parameters.
func (cli *Client) CreateGroup(req ReqCreateGroup) (*types.GroupInfo, error) {
participantNodes := make([]waBinary.Node, len(req.Participants), len(req.Participants)+1)
for i, participant := range req.Participants {
participantNodes[i] = waBinary.Node{
Tag: "participant",
Attrs: waBinary.Attrs{"jid": participant},
}
}
if createKey == "" {
createKey = GenerateMessageID()
if req.CreateKey == "" {
req.CreateKey = GenerateMessageID()
}
if req.IsParent {
if req.DefaultMembershipApprovalMode == "" {
req.DefaultMembershipApprovalMode = "request_required"
}
participantNodes = append(participantNodes, waBinary.Node{
Tag: "parent",
Attrs: waBinary.Attrs{
"default_membership_approval_mode": req.DefaultMembershipApprovalMode,
},
})
} else if !req.LinkedParentJID.IsEmpty() {
participantNodes = append(participantNodes, waBinary.Node{
Tag: "linked_parent",
Attrs: waBinary.Attrs{"jid": req.LinkedParentJID},
})
}
// WhatsApp web doesn't seem to include the static prefix for these
key := strings.TrimPrefix(createKey, "3EB0")
key := strings.TrimPrefix(req.CreateKey, "3EB0")
resp, err := cli.sendGroupIQ(context.TODO(), iqSet, types.GroupServerJID, waBinary.Node{
Tag: "create",
Attrs: waBinary.Attrs{
"subject": name,
"subject": req.Name,
"key": key,
},
Content: participantNodes,
@@ -68,6 +95,37 @@ func (cli *Client) CreateGroup(name string, participants []types.JID, createKey
return cli.parseGroupNode(&groupNode)
}
// UnlinkGroup removes a child group from a parent community.
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},
Content: []waBinary.Node{{
Tag: "group",
Attrs: waBinary.Attrs{"jid": child},
}},
})
return err
}
// LinkGroup adds an existing group as a child group in a community.
//
// To create a new group within a community, set LinkedParentJID in the CreateGroup request.
func (cli *Client) LinkGroup(parent, child types.JID) error {
_, err := cli.sendGroupIQ(context.TODO(), iqSet, parent, waBinary.Node{
Tag: "links",
Content: []waBinary.Node{{
Tag: "link",
Attrs: waBinary.Attrs{"link_type": types.GroupLinkChangeTypeSub},
Content: []waBinary.Node{{
Tag: "group",
Attrs: waBinary.Attrs{"jid": child},
}},
}},
})
return err
}
// LeaveGroup leaves the specified group on WhatsApp.
func (cli *Client) LeaveGroup(jid types.JID) error {
_, err := cli.sendGroupIQ(context.TODO(), iqSet, types.GroupServerJID, waBinary.Node{
@@ -357,6 +415,42 @@ func (cli *Client) GetJoinedGroups() ([]*types.GroupInfo, error) {
return infos, nil
}
// GetSubGroups gets the subgroups of the given community.
func (cli *Client) GetSubGroups(community types.JID) ([]*types.GroupLinkTarget, error) {
res, err := cli.sendGroupIQ(context.TODO(), iqGet, community, waBinary.Node{Tag: "sub_groups"})
if err != nil {
return nil, err
}
groups, ok := res.GetOptionalChildByTag("sub_groups")
if !ok {
return nil, &ElementMissingError{Tag: "sub_groups", In: "response to subgroups query"}
}
var parsedGroups []*types.GroupLinkTarget
for _, child := range groups.GetChildren() {
if child.Tag == "group" {
parsedGroup, err := parseGroupLinkTargetNode(&child)
if err != nil {
return parsedGroups, fmt.Errorf("failed to parse group in subgroups list: %w", err)
}
parsedGroups = append(parsedGroups, &parsedGroup)
}
}
return parsedGroups, nil
}
// GetLinkedGroupsParticipants gets all the participants in the groups of the given community.
func (cli *Client) GetLinkedGroupsParticipants(community types.JID) ([]types.JID, error) {
res, err := cli.sendGroupIQ(context.TODO(), iqGet, community, waBinary.Node{Tag: "linked_groups_participants"})
if err != nil {
return nil, err
}
participants, ok := res.GetOptionalChildByTag("linked_groups_participants")
if !ok {
return nil, &ElementMissingError{Tag: "linked_groups_participants", In: "response to community participants query"}
}
return parseParticipantList(&participants), nil
}
// GetGroupInfo requests basic info about a group chat from the WhatsApp servers.
func (cli *Client) GetGroupInfo(jid types.JID) (*types.GroupInfo, error) {
return cli.getGroupInfo(context.TODO(), jid, true)
@@ -433,6 +527,17 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
IsSuperAdmin: pcpType == "superadmin",
JID: childAG.JID("jid"),
}
if errorCode := childAG.OptionalInt("error"); errorCode != 0 {
participant.Error = errorCode
addRequest, ok := child.GetOptionalChildByTag("add_request")
if ok {
addAG := addRequest.AttrGetter()
participant.AddRequest = &types.GroupParticipantAddRequest{
Code: addAG.String("code"),
Expiration: addAG.UnixTime("expiration"),
}
}
}
group.Participants = append(group.Participants, participant)
case "description":
body, bodyOK := child.GetOptionalChildByTag("body")
@@ -453,6 +558,13 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
case "member_add_mode":
modeBytes, _ := child.Content.([]byte)
group.MemberAddMode = types.GroupMemberAddMode(modeBytes)
case "linked_parent":
group.LinkedParentJID = childAG.JID("jid")
case "default_sub_group":
group.IsDefaultSubGroup = true
case "parent":
group.IsParent = true
group.DefaultMembershipApprovalMode = childAG.OptionalString("default_membership_approval_mode")
default:
cli.Log.Debugf("Unknown element in group node %s: %s", group.JID.String(), child.XMLString())
}
@@ -464,6 +576,24 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
return &group, ag.Error()
}
func parseGroupLinkTargetNode(groupNode *waBinary.Node) (types.GroupLinkTarget, error) {
ag := groupNode.AttrGetter()
jidKey := ag.OptionalJIDOrEmpty("jid")
if jidKey.IsEmpty() {
jidKey = types.NewJID(ag.String("id"), types.GroupServer)
}
return types.GroupLinkTarget{
JID: jidKey,
GroupName: types.GroupName{
Name: ag.String("subject"),
NameSetAt: ag.UnixTime("s_t"),
},
GroupIsDefaultSub: types.GroupIsDefaultSub{
IsDefaultSubGroup: groupNode.GetChildByTag("default_sub_group").Tag == "default_sub_group",
},
}, ag.Error()
}
func parseParticipantList(node *waBinary.Node) (participants []types.JID) {
children := node.GetChildren()
participants = make([]types.JID, 0, len(children))
@@ -526,6 +656,8 @@ func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, err
evt.Locked = &types.GroupLocked{IsLocked: true}
case "unlocked":
evt.Locked = &types.GroupLocked{IsLocked: false}
case "delete":
evt.Delete = &types.GroupDelete{Deleted: true, DeleteReason: cag.String("reason")}
case "subject":
evt.Name = &types.GroupName{
Name: cag.String("subject"),
@@ -575,6 +707,33 @@ func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, err
}
case "not_ephemeral":
evt.Ephemeral = &types.GroupEphemeral{IsEphemeral: false}
case "link":
evt.Link = &types.GroupLinkChange{
Type: types.GroupLinkChangeType(cag.String("link_type")),
}
groupNode, ok := child.GetOptionalChildByTag("group")
if !ok {
return nil, &ElementMissingError{Tag: "group", In: "group link"}
}
var err error
evt.Link.Group, err = parseGroupLinkTargetNode(&groupNode)
if err != nil {
return nil, fmt.Errorf("failed to parse group link node in group change: %w", err)
}
case "unlink":
evt.Unlink = &types.GroupLinkChange{
Type: types.GroupLinkChangeType(cag.String("unlink_type")),
UnlinkReason: types.GroupUnlinkReason(cag.String("unlink_reason")),
}
groupNode, ok := child.GetOptionalChildByTag("group")
if !ok {
return nil, &ElementMissingError{Tag: "group", In: "group unlink"}
}
var err error
evt.Unlink.Group, err = parseGroupLinkTargetNode(&groupNode)
if err != nil {
return nil, fmt.Errorf("failed to parse group unlink node in group change: %w", err)
}
default:
evt.UnknownChanges = append(evt.UnknownChanges, &child)
}
+5 -1
View File
@@ -82,6 +82,10 @@ func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []
if err != nil {
return fmt.Errorf("failed to prepare encrypted retry receipt: %w", err)
}
ownID := cli.getOwnID().ToNonAD()
if ownID.IsEmpty() {
return ErrNotLoggedIn
}
rmrAttrs := waBinary.Attrs{
"jid": message.Chat,
@@ -100,7 +104,7 @@ func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []
Tag: "receipt",
Attrs: waBinary.Attrs{
"id": message.ID,
"to": cli.Store.ID.ToNonAD(),
"to": ownID,
"type": "server-error",
},
Content: []waBinary.Node{
+28 -4
View File
@@ -51,8 +51,8 @@ func (cli *Client) handleEncryptedMessage(node *waBinary.Node) {
}
func (cli *Client) parseMessageSource(node *waBinary.Node, requireParticipant bool) (source types.MessageSource, err error) {
clientID := cli.Store.ID
if clientID == nil {
clientID := cli.getOwnID()
if clientID.IsEmpty() {
err = ErrNotLoggedIn
return
}
@@ -406,18 +406,33 @@ func (cli *Client) processProtocolParts(info *types.MessageInfo, msg *waProto.Me
func (cli *Client) storeHistoricalMessageSecrets(conversations []*waProto.Conversation) {
var secrets []store.MessageSecretInsert
me := cli.Store.ID.ToNonAD()
var privacyTokens []store.PrivacyToken
ownID := cli.getOwnID().ToNonAD()
if ownID.IsEmpty() {
return
}
for _, conv := range conversations {
chatJID, _ := types.ParseJID(conv.GetId())
if chatJID.IsEmpty() {
continue
}
if chatJID.Server == types.DefaultUserServer && conv.GetTcToken() != nil {
ts := conv.GetTcTokenSenderTimestamp()
if ts == 0 {
ts = conv.GetTcTokenTimestamp()
}
privacyTokens = append(privacyTokens, store.PrivacyToken{
User: chatJID,
Token: conv.GetTcToken(),
Timestamp: time.Unix(int64(ts), 0),
})
}
for _, msg := range conv.GetMessages() {
if secret := msg.GetMessage().GetMessageSecret(); secret != nil {
var senderJID types.JID
msgKey := msg.GetMessage().GetKey()
if msgKey.GetFromMe() {
senderJID = me
senderJID = ownID
} else if chatJID.Server == types.DefaultUserServer {
senderJID = chatJID
} else if msgKey.GetParticipant() != "" {
@@ -446,6 +461,15 @@ func (cli *Client) storeHistoricalMessageSecrets(conversations []*waProto.Conver
cli.Log.Infof("Stored %d message secret keys from history sync", len(secrets))
}
}
if len(privacyTokens) > 0 {
cli.Log.Debugf("Storing %d privacy tokens in history sync", len(privacyTokens))
err := cli.Store.PrivacyTokens.PutPrivacyTokens(privacyTokens...)
if err != nil {
cli.Log.Errorf("Failed to store privacy tokens in history sync: %v", err)
} else {
cli.Log.Infof("Stored %d privacy tokens from history sync", len(privacyTokens))
}
}
}
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message) {
+6 -3
View File
@@ -94,7 +94,10 @@ func (cli *Client) decryptMsgSecret(msg *events.Message, useCase MsgSecretType,
}
func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.MessageID, useCase MsgSecretType, plaintext []byte) (ciphertext, iv []byte, err error) {
ownID := *cli.Store.ID
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return nil, nil, ErrNotLoggedIn
}
baseEncKey, err := cli.Store.MsgSecrets.GetMessageSecret(chat, origSender, origMsgID)
if err != nil {
@@ -208,7 +211,7 @@ func HashPollOptions(optionNames []string) [][]byte {
// fmt.Println(":(", err)
// return
// }
// resp, err := cli.SendMessage(context.Background(), evt.Info.Chat, "", pollVoteMsg)
// resp, err := cli.SendMessage(context.Background(), evt.Info.Chat, pollVoteMsg)
// }
func (cli *Client) BuildPollVote(pollInfo *types.MessageInfo, optionNames []string) (*waProto.Message, error) {
pollUpdate, err := cli.EncryptPollVote(pollInfo, &waProto.PollVoteMessage{
@@ -220,7 +223,7 @@ func (cli *Client) BuildPollVote(pollInfo *types.MessageInfo, optionNames []stri
// BuildPollCreation builds a poll creation message with the given poll name, options and maximum number of selections.
// The built message can be sent normally using Client.SendMessage.
//
// resp, err := cli.SendMessage(context.Background(), chat, "", cli.BuildPollCreation("meow?", []string{"yes", "no"}, 1))
// 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)
+58 -4
View File
@@ -11,6 +11,7 @@ import (
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
@@ -135,7 +136,12 @@ func (cli *Client) handleDeviceNotification(node *waBinary.Node) {
func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) {
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
cached, ok := cli.userDevicesCache[cli.Store.ID.ToNonAD()]
ownID := cli.getOwnID().ToNonAD()
if ownID.IsEmpty() {
cli.Log.Debugf("Ignoring own device change notification, session was deleted")
return
}
cached, ok := cli.userDevicesCache[ownID]
if !ok {
cli.Log.Debugf("Ignoring own device change notification, device list not cached")
return
@@ -153,10 +159,10 @@ func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) {
newHash := participantListHashV2(newDeviceList)
if newHash != expectedNewHash {
cli.Log.Debugf("Received own device list change notification %s -> %s, but expected hash was %s", oldHash, newHash, expectedNewHash)
delete(cli.userDevicesCache, cli.Store.ID.ToNonAD())
delete(cli.userDevicesCache, ownID)
} else {
cli.Log.Debugf("Received own device list change notification %s -> %s", oldHash, newHash)
cli.userDevicesCache[cli.Store.ID.ToNonAD()] = newDeviceList
cli.userDevicesCache[ownID] = newDeviceList
}
}
@@ -173,6 +179,52 @@ func (cli *Client) handleAccountSyncNotification(node *waBinary.Node) {
}
}
func (cli *Client) handlePrivacyTokenNotification(node *waBinary.Node) {
ownID := cli.getOwnID().ToNonAD()
if ownID.IsEmpty() {
cli.Log.Debugf("Ignoring privacy token notification, session was deleted")
return
}
tokens := node.GetChildByTag("tokens")
if tokens.Tag != "tokens" {
cli.Log.Warnf("privacy_token notification didn't contain <tokens> tag")
return
}
parentAG := node.AttrGetter()
sender := parentAG.JID("from")
if !parentAG.OK() {
cli.Log.Warnf("privacy_token notification didn't have a sender (%v)", parentAG.Error())
return
}
for _, child := range tokens.GetChildren() {
ag := child.AttrGetter()
if child.Tag != "token" {
cli.Log.Warnf("privacy_token notification contained unexpected <%s> tag", child.Tag)
} else if targetUser := ag.JID("jid"); targetUser != ownID {
cli.Log.Warnf("privacy_token notification contained token for different user %s", targetUser)
} else if tokenType := ag.String("type"); tokenType != "trusted_contact" {
cli.Log.Warnf("privacy_token notification contained unexpected token type %s", tokenType)
} else if token, ok := child.Content.([]byte); !ok {
cli.Log.Warnf("privacy_token notification contained non-binary token")
} else {
timestamp := ag.UnixTime("t")
if !ag.OK() {
cli.Log.Warnf("privacy_token notification is missing some fields: %v", ag.Error())
}
err := cli.Store.PrivacyTokens.PutPrivacyTokens(store.PrivacyToken{
User: sender,
Token: token,
Timestamp: timestamp,
})
if err != nil {
cli.Log.Errorf("Failed to save privacy token from %s: %v", sender, err)
} else {
cli.Log.Debugf("Stored privacy token from %s (ts: %v)", sender, timestamp)
}
}
}
}
func (cli *Client) handleNotification(node *waBinary.Node) {
ag := node.AttrGetter()
notifType := ag.String("type")
@@ -200,7 +252,9 @@ func (cli *Client) handleNotification(node *waBinary.Node) {
go cli.handlePictureNotification(node)
case "mediaretry":
go cli.handleMediaRetryNotification(node)
// Other types: business, disappearing_mode, server, status, pay, psa, privacy_token
case "privacy_token":
go cli.handlePrivacyTokenNotification(node)
// Other types: business, disappearing_mode, server, status, pay, psa
default:
cli.Log.Debugf("Unhandled notification with type %s", notifType)
}
+27 -23
View File
@@ -13,11 +13,9 @@ import (
"encoding/base64"
"fmt"
"strings"
"time"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/ecc"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
@@ -26,8 +24,6 @@ import (
"go.mau.fi/whatsmeow/util/keys"
)
const qrScanTimeout = 30 * time.Second
func (cli *Client) handleIQ(node *waBinary.Node) {
children := node.GetChildren()
if len(children) != 1 || node.Attrs["from"] != types.ServerJID {
@@ -105,28 +101,28 @@ func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, p
var deviceIdentityContainer waProto.ADVSignedDeviceIdentityHMAC
err := proto.Unmarshal(deviceIdentityBytes, &deviceIdentityContainer)
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to parse device identity container in pair success message: %w", err)
cli.sendPairError(reqID, 500, "internal-error")
return &PairProtoError{"failed to parse device identity container in pair success message", err}
}
h := hmac.New(sha256.New, cli.Store.AdvSecretKey)
h.Write(deviceIdentityContainer.Details)
if !bytes.Equal(h.Sum(nil), deviceIdentityContainer.Hmac) {
cli.Log.Warnf("Invalid HMAC from pair success message")
cli.sendIQError(reqID, 401, "not-authorized")
return fmt.Errorf("invalid device identity HMAC in pair success message")
cli.sendPairError(reqID, 401, "not-authorized")
return ErrPairInvalidDeviceIdentityHMAC
}
var deviceIdentity waProto.ADVSignedDeviceIdentity
err = proto.Unmarshal(deviceIdentityContainer.Details, &deviceIdentity)
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to parse signed device identity in pair success message: %w", err)
cli.sendPairError(reqID, 500, "internal-error")
return &PairProtoError{"failed to parse signed device identity in pair success message", err}
}
if !verifyDeviceIdentityAccountSignature(&deviceIdentity, cli.Store.IdentityKey) {
cli.sendIQError(reqID, 401, "not-authorized")
return fmt.Errorf("invalid device signature in pair success message")
cli.sendPairError(reqID, 401, "not-authorized")
return ErrPairInvalidDeviceSignature
}
deviceIdentity.DeviceSignature = generateDeviceSignature(&deviceIdentity, cli.Store.IdentityKey)[:]
@@ -134,8 +130,13 @@ func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, p
var deviceIdentityDetails waProto.ADVDeviceIdentity
err = proto.Unmarshal(deviceIdentity.Details, &deviceIdentityDetails)
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to parse device identity details in pair success message: %w", err)
cli.sendPairError(reqID, 500, "internal-error")
return &PairProtoError{"failed to parse device identity details in pair success message", err}
}
if cli.PrePairCallback != nil && !cli.PrePairCallback(jid, platform, businessName) {
cli.sendPairError(reqID, 500, "internal-error")
return ErrPairRejectedLocally
}
cli.Store.Account = proto.Clone(&deviceIdentity).(*waProto.ADVSignedDeviceIdentity)
@@ -147,8 +148,8 @@ func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, p
selfSignedDeviceIdentity, err := proto.Marshal(&deviceIdentity)
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to marshal self-signed device identity: %w", err)
cli.sendPairError(reqID, 500, "internal-error")
return &PairProtoError{"failed to marshal self-signed device identity", err}
}
cli.Store.ID = &jid
@@ -156,14 +157,14 @@ func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, p
cli.Store.Platform = platform
err = cli.Store.Save()
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to save device store: %w", err)
cli.sendPairError(reqID, 500, "internal-error")
return &PairDatabaseError{"failed to save device store", err}
}
err = cli.Store.Identities.PutIdentity(mainDeviceJID.SignalAddress().String(), mainDeviceIdentity)
if err != nil {
_ = cli.Store.Delete()
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to store main device identity: %w", err)
cli.sendPairError(reqID, 500, "internal-error")
return &PairDatabaseError{"failed to store main device identity", err}
}
// Expect a disconnect after this and don't dispatch the usual Disconnected event
@@ -225,8 +226,8 @@ func generateDeviceSignature(deviceIdentity *waProto.ADVSignedDeviceIdentity, ik
return &sig
}
func (cli *Client) sendIQError(id string, code int, text string) waBinary.Node {
return waBinary.Node{
func (cli *Client) sendPairError(id string, code int, text string) {
err := cli.sendNode(waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": types.ServerJID,
@@ -240,5 +241,8 @@ func (cli *Client) sendIQError(id string, code int, text string) waBinary.Node {
"text": text,
},
}},
})
if err != nil {
cli.Log.Errorf("Failed to send pair error node: %v", err)
}
}
+25 -3
View File
@@ -7,6 +7,7 @@
package whatsmeow
import (
"fmt"
"sync/atomic"
waBinary "go.mau.fi/whatsmeow/binary"
@@ -87,19 +88,40 @@ func (cli *Client) SendPresence(state types.Presence) error {
//
// cli.SendPresence(types.PresenceAvailable)
func (cli *Client) SubscribePresence(jid types.JID) error {
return cli.sendNode(waBinary.Node{
privacyToken, err := cli.Store.PrivacyTokens.GetPrivacyToken(jid)
if err != nil {
return fmt.Errorf("failed to get privacy token: %w", err)
} else if privacyToken == nil {
if cli.ErrorOnSubscribePresenceWithoutToken {
return fmt.Errorf("%w for %v", ErrNoPrivacyToken, jid.ToNonAD())
} else {
cli.Log.Debugf("Trying to subscribe to presence of %s without privacy token", jid)
}
}
req := waBinary.Node{
Tag: "presence",
Attrs: waBinary.Attrs{
"type": "subscribe",
"to": jid,
},
})
}
if privacyToken != nil {
req.Content = []waBinary.Node{{
Tag: "tctoken",
Content: privacyToken.Token,
}}
}
return cli.sendNode(req)
}
// SendChatPresence updates the user's typing status in a specific chat.
//
// The media parameter can be set to indicate the user is recording media (like a voice message) rather than typing a text message.
func (cli *Client) SendChatPresence(jid types.JID, state types.ChatPresence, media types.ChatPresenceMedia) error {
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return ErrNotLoggedIn
}
content := []waBinary.Node{{Tag: string(state)}}
if state == types.ChatPresenceComposing && len(media) > 0 {
content[0].Attrs = waBinary.Attrs{
@@ -109,7 +131,7 @@ func (cli *Client) SendChatPresence(jid types.JID, state types.ChatPresence, med
return cli.sendNode(waBinary.Node{
Tag: "chatstate",
Attrs: waBinary.Attrs{
"from": *cli.Store.ID,
"from": ownID,
"to": jid,
},
Content: content,
+9 -4
View File
@@ -17,7 +17,7 @@ import (
)
type QRChannelItem struct {
// The type of event, "code" for new QR codes.
// The type of event, "code" for new QR codes (see Code field) and "error" for pairing errors (see Error) field.
// For non-code/error events, you can just compare the whole item to the event variables (like QRChannelSuccess).
Event string
// If the item is a pair error, then this field contains the error message.
@@ -28,6 +28,11 @@ type QRChannelItem struct {
Timeout time.Duration
}
const QRChannelEventCode = "code"
const QRChannelEventError = "error"
// Possible final items in the QR channel. In addition to these, an `error` event may be emitted,
// in which case the Error field will have the error that occurred during pairing.
var (
// QRChannelSuccess is emitted from GetQRChannel when the pairing is successful.
QRChannelSuccess = QRChannelItem{Event: "success"}
@@ -78,7 +83,7 @@ func (qrc *qrChannel) emitQRs(evt *events.QR) {
nextCode, evt.Codes = evt.Codes[0], evt.Codes[1:]
qrc.log.Debugf("Emitting QR code %s", nextCode)
select {
case qrc.output <- QRChannelItem{Code: nextCode, Timeout: timeout, Event: "code"}:
case qrc.output <- QRChannelItem{Code: nextCode, Timeout: timeout, Event: QRChannelEventCode}:
default:
qrc.log.Debugf("Output channel didn't accept code, exiting QR emitter")
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
@@ -125,7 +130,7 @@ func (qrc *qrChannel) handleEvent(rawEvt interface{}) {
outputType = QRChannelSuccess
case *events.PairError:
outputType = QRChannelItem{
Event: "error",
Event: QRChannelEventError,
Error: evt.Error,
}
case *events.Disconnected:
@@ -151,7 +156,7 @@ func (qrc *qrChannel) handleEvent(rawEvt interface{}) {
//
// This must be called *before* Connect(). It will then listen to all the relevant events from the client.
//
// The last value to be emitted will be a special string, either "success", "timeout" or "err-already-have-id",
// The last value to be emitted will be a special event like "success", "timeout" or another error code
// depending on the result of the pairing. The channel will be closed immediately after one of those.
func (cli *Client) GetQRChannel(ctx context.Context) (<-chan QRChannelItem, error) {
if cli.IsConnected() {
+5 -1
View File
@@ -111,10 +111,14 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if err != nil {
return err
}
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return ErrNotLoggedIn
}
if receipt.IsGroup {
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), cli.Store.ID.SignalAddress())
senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), ownID.SignalAddress())
signalSKDMessage, err := builder.Create(senderKeyName)
if err != nil {
cli.Log.Warnf("Failed to create sender key distribution message to include in retry of %s in %s to %s: %v", messageID, receipt.Chat, receipt.Sender, err)
+76 -41
View File
@@ -35,7 +35,7 @@ import (
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
//
// msgID := whatsmeow.GenerateMessageID()
// cli.SendMessage(context.Background(), targetJID, msgID, &waProto.Message{...})
// cli.SendMessage(context.Background(), targetJID, &waProto.Message{...}, whatsmeow.SendRequestExtra{ID: msgID})
func GenerateMessageID() types.MessageID {
id := make([]byte, 8)
_, err := rand.Read(id)
@@ -71,17 +71,36 @@ type SendResponse struct {
DebugTimings MessageDebugTimings
}
// SendMessage sends the given message.
// SendRequestExtra contains the optional parameters for SendMessage.
//
// If the message ID is not provided, a random message ID will be generated.
// By default, optional parameters don't have to be provided at all, e.g.
//
// cli.SendMessage(ctx, to, message)
//
// When providing optional parameters, add a single instance of this struct as the last parameter:
//
// cli.SendMessage(ctx, to, message, whatsmeow.SendRequestExtra{...})
//
// Trying to add multiple extra parameters will return an error.
type SendRequestExtra struct {
// The message ID to use when sending. If this is not provided, a random message ID will be generated
ID types.MessageID
// Should the message be sent as a peer message (protocol messages to your own devices, e.g. app state key requests)
Peer bool
}
// SendMessage sends the given message.
//
// This method will wait for the server to acknowledge the message before returning.
// The return value is the timestamp of the message from the server.
//
// Optional parameters like the message ID can be specified with the SendRequestExtra struct.
// Only one extra parameter is allowed, put all necessary parameters in the same struct.
//
// The message itself can contain anything you want (within the protobuf schema).
// e.g. for a simple text message, use the Conversation field:
//
// cli.SendMessage(context.Background(), targetJID, "", &waProto.Message{
// cli.SendMessage(context.Background(), targetJID, &waProto.Message{
// Conversation: proto.String("Hello, World!"),
// })
//
@@ -91,18 +110,31 @@ type SendResponse struct {
// For uploading and sending media/attachments, see the Upload method.
//
// For other message types, you'll have to figure it out yourself. Looking at the protobuf schema
// in binary/proto/def.proto may be useful to find out all the allowed fields.
func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.MessageID, message *waProto.Message) (resp SendResponse, err error) {
isPeerMessage := to.User == cli.Store.ID.User
if to.AD && !isPeerMessage {
// in binary/proto/def.proto may be useful to find out all the allowed fields. Printing the RawMessage
// field in incoming message events to figure out what it contains is also a good way to learn how to
// send the same kind of message.
func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waProto.Message, extra ...SendRequestExtra) (resp SendResponse, err error) {
var req SendRequestExtra
if len(extra) > 1 {
err = errors.New("only one extra parameter may be provided to SendMessage")
return
} else if len(extra) == 1 {
req = extra[0]
}
if to.AD && !req.Peer {
err = ErrRecipientADJID
return
}
if len(id) == 0 {
id = GenerateMessageID()
ownID := cli.getOwnID()
if ownID.IsEmpty() {
err = ErrNotLoggedIn
return
}
resp.ID = id
if len(req.ID) == 0 {
req.ID = GenerateMessageID()
}
resp.ID = req.ID
start := time.Now()
// Sending multiple messages at a time can cause weird issues and makes it harder to retry safely
@@ -110,36 +142,36 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.Messa
resp.DebugTimings.Queue = time.Since(start)
defer cli.messageSendLock.Unlock()
respChan := cli.waitResponse(id)
respChan := cli.waitResponse(req.ID)
// Peer message retries aren't implemented yet
if !isPeerMessage {
cli.addRecentMessage(to, id, message)
if !req.Peer {
cli.addRecentMessage(to, req.ID, message)
}
if message.GetMessageContextInfo().GetMessageSecret() != nil {
err = cli.Store.MsgSecrets.PutMessageSecret(to, *cli.Store.ID, id, message.GetMessageContextInfo().GetMessageSecret())
err = cli.Store.MsgSecrets.PutMessageSecret(to, ownID, req.ID, message.GetMessageContextInfo().GetMessageSecret())
if err != nil {
cli.Log.Warnf("Failed to store message secret key for outgoing message %s: %v", id, err)
cli.Log.Warnf("Failed to store message secret key for outgoing message %s: %v", req.ID, err)
} else {
cli.Log.Debugf("Stored message secret key for outgoing message %s", id)
cli.Log.Debugf("Stored message secret key for outgoing message %s", req.ID)
}
}
var phash string
var data []byte
switch to.Server {
case types.GroupServer, types.BroadcastServer:
phash, data, err = cli.sendGroup(ctx, to, id, message, &resp.DebugTimings)
phash, data, err = cli.sendGroup(ctx, to, ownID, req.ID, message, &resp.DebugTimings)
case types.DefaultUserServer:
if isPeerMessage {
data, err = cli.sendPeerMessage(to, id, message, &resp.DebugTimings)
if req.Peer {
data, err = cli.sendPeerMessage(to, req.ID, message, &resp.DebugTimings)
} else {
data, err = cli.sendDM(ctx, to, id, message, &resp.DebugTimings)
data, err = cli.sendDM(ctx, to, ownID, req.ID, message, &resp.DebugTimings)
}
default:
err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server)
}
start = time.Now()
if err != nil {
cli.cancelResponse(id, respChan)
cli.cancelResponse(req.ID, respChan)
return
}
var respNode *waBinary.Node
@@ -152,7 +184,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.Messa
resp.DebugTimings.Resp = time.Since(start)
if isDisconnectNode(respNode) {
start = time.Now()
respNode, err = cli.retryFrame("message send", id, data, respNode, ctx, 0)
respNode, err = cli.retryFrame("message send", req.ID, data, respNode, ctx, 0)
resp.DebugTimings.Retry = time.Since(start)
if err != nil {
return
@@ -160,6 +192,9 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.Messa
}
ag := respNode.AttrGetter()
resp.Timestamp = ag.UnixTime("t")
if errorCode := ag.Int("error"); errorCode != 0 {
err = fmt.Errorf("%w %d", ErrServerReturnedError, errorCode)
}
expectedPHash := ag.OptionalString("phash")
if len(expectedPHash) > 0 && phash != expectedPHash {
cli.Log.Warnf("Server returned different participant list hash when sending to %s. Some devices may not have received the message.", to)
@@ -178,7 +213,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, id types.Messa
//
// Deprecated: This method is deprecated in favor of BuildRevoke
func (cli *Client) RevokeMessage(chat types.JID, id types.MessageID) (SendResponse, error) {
return cli.SendMessage(context.TODO(), chat, "", cli.BuildRevoke(chat, types.EmptyJID, id))
return cli.SendMessage(context.TODO(), chat, cli.BuildRevoke(chat, types.EmptyJID, id))
}
// BuildRevoke builds a message revocation message using the given variables.
@@ -186,18 +221,18 @@ func (cli *Client) RevokeMessage(chat types.JID, id types.MessageID) (SendRespon
//
// 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)
// 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)
// 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 {
key := &waProto.MessageKey{
FromMe: proto.Bool(true),
Id: proto.String(id),
RemoteJid: proto.String(chat.String()),
}
if !sender.IsEmpty() && sender.User != cli.Store.ID.User {
if !sender.IsEmpty() && sender.User != cli.getOwnID().User {
key.FromMe = proto.Bool(false)
if chat.Server != types.DefaultUserServer {
key.Participant = proto.String(sender.ToNonAD().String())
@@ -214,7 +249,7 @@ func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waPr
// BuildEdit builds a message edit message using the given variables.
// The built message can be sent normally using Client.SendMessage.
//
// resp, err := cli.SendMessage(context.Background(), chat, "", cli.BuildEdit(chat, originalMessageID, &waProto.Message{
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildEdit(chat, originalMessageID, &waProto.Message{
// Conversation: proto.String("edited message"),
// })
func (cli *Client) BuildEdit(chat types.JID, id types.MessageID, newContent *waProto.Message) *waProto.Message {
@@ -270,7 +305,7 @@ func ParseDisappearingTimerString(val string) (time.Duration, bool) {
func (cli *Client) SetDisappearingTimer(chat types.JID, timer time.Duration) (err error) {
switch chat.Server {
case types.DefaultUserServer:
_, err = cli.SendMessage(context.TODO(), chat, "", &waProto.Message{
_, err = cli.SendMessage(context.TODO(), chat, &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_EPHEMERAL_SETTING.Enum(),
EphemeralExpiration: proto.Uint32(uint32(timer.Seconds())),
@@ -307,7 +342,7 @@ func participantListHashV2(participants []types.JID) string {
return fmt.Sprintf("2:%s", base64.RawStdEncoding.EncodeToString(hash[:6]))
}
func (cli *Client) sendGroup(ctx context.Context, to types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (string, []byte, error) {
func (cli *Client) sendGroup(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (string, []byte, error) {
var participants []types.JID
var err error
start := time.Now()
@@ -333,7 +368,7 @@ func (cli *Client) sendGroup(ctx context.Context, to types.JID, id types.Message
start = time.Now()
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(to.String(), cli.Store.ID.SignalAddress())
senderKeyName := protocol.NewSenderKeyName(to.String(), ownID.SignalAddress())
signalSKDMessage, err := builder.Create(senderKeyName)
if err != nil {
return "", nil, fmt.Errorf("failed to create sender key distribution message to send %s to %s: %w", id, to, err)
@@ -357,7 +392,7 @@ func (cli *Client) sendGroup(ctx context.Context, to types.JID, id types.Message
ciphertext := encrypted.SignedSerialize()
timings.GroupEncrypt = time.Since(start)
node, allDevices, err := cli.prepareMessageNode(ctx, to, id, message, participants, skdPlaintext, nil, timings)
node, allDevices, err := cli.prepareMessageNode(ctx, to, ownID, id, message, participants, skdPlaintext, nil, timings)
if err != nil {
return "", nil, err
}
@@ -393,7 +428,7 @@ func (cli *Client) sendPeerMessage(to types.JID, id types.MessageID, message *wa
return data, nil
}
func (cli *Client) sendDM(ctx context.Context, to types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) ([]byte, error) {
func (cli *Client) sendDM(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) ([]byte, error) {
start := time.Now()
messagePlaintext, deviceSentMessagePlaintext, err := marshalMessage(to, message)
timings.Marshal = time.Since(start)
@@ -401,7 +436,7 @@ func (cli *Client) sendDM(ctx context.Context, to types.JID, id types.MessageID,
return nil, err
}
node, _, err := cli.prepareMessageNode(ctx, to, id, message, []types.JID{to, cli.Store.ID.ToNonAD()}, messagePlaintext, deviceSentMessagePlaintext, timings)
node, _, err := cli.prepareMessageNode(ctx, to, ownID, id, message, []types.JID{to, ownID.ToNonAD()}, messagePlaintext, deviceSentMessagePlaintext, timings)
if err != nil {
return nil, err
}
@@ -504,7 +539,7 @@ func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, mess
}, nil
}
func (cli *Client) prepareMessageNode(ctx context.Context, to types.JID, id types.MessageID, message *waProto.Message, participants []types.JID, plaintext, dsmPlaintext []byte, timings *MessageDebugTimings) (*waBinary.Node, []types.JID, error) {
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)
@@ -522,7 +557,7 @@ func (cli *Client) prepareMessageNode(ctx context.Context, to types.JID, id type
}
start = time.Now()
participantNodes, includeIdentity := cli.encryptMessageForDevices(ctx, allDevices, id, plaintext, dsmPlaintext)
participantNodes, includeIdentity := cli.encryptMessageForDevices(ctx, allDevices, ownID, id, plaintext, dsmPlaintext)
timings.PeerEncrypt = time.Since(start)
content := []waBinary.Node{{
Tag: "participants",
@@ -584,14 +619,14 @@ func (cli *Client) makeDeviceIdentityNode() waBinary.Node {
}
}
func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []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) ([]waBinary.Node, bool) {
includeIdentity := false
participantNodes := make([]waBinary.Node, 0, len(allDevices))
var retryDevices []types.JID
for _, jid := range allDevices {
plaintext := msgPlaintext
if jid.User == cli.Store.ID.User && dsmPlaintext != nil {
if jid == *cli.Store.ID {
if jid.User == ownID.User && dsmPlaintext != nil {
if jid == ownID {
continue
}
plaintext = dsmPlaintext
@@ -621,7 +656,7 @@ func (cli *Client) encryptMessageForDevices(ctx context.Context, allDevices []ty
continue
}
plaintext := msgPlaintext
if jid.User == cli.Store.ID.User && dsmPlaintext != nil {
if jid.User == ownID.User && dsmPlaintext != nil {
plaintext = dsmPlaintext
}
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, resp.bundle)
+1 -1
View File
@@ -74,7 +74,7 @@ func (vc WAVersionContainer) ProtoAppVersion() *waProto.ClientPayload_UserAgent_
}
// waVersion is the WhatsApp web client version
var waVersion = WAVersionContainer{2, 2245, 9}
var waVersion = WAVersionContainer{2, 2301, 6}
// waVersionHash is the md5 hash of a dot-separated waVersion
var waVersionHash [16]byte
@@ -126,6 +126,7 @@ func (c *Container) scanDevice(row scannable) (*store.Device, error) {
device.Contacts = innerStore
device.ChatSettings = innerStore
device.MsgSecrets = innerStore
device.PrivacyTokens = innerStore
device.Container = c
device.Initialized = true
@@ -240,6 +241,7 @@ func (c *Container) PutDevice(device *store.Device) error {
device.Contacts = innerStore
device.ChatSettings = innerStore
device.MsgSecrets = innerStore
device.PrivacyTokens = innerStore
device.Initialized = true
}
return err
+40 -1
View File
@@ -706,7 +706,7 @@ func (s *SQLStore) PutMessageSecrets(inserts []store.MessageSecretInsert) (err e
return fmt.Errorf("failed to begin transaction: %w", err)
}
for _, insert := range inserts {
_, err = s.db.Exec(putMsgSecret, s.JID, insert.Chat.ToNonAD(), insert.Sender.ToNonAD(), insert.ID, insert.Secret)
_, err = tx.Exec(putMsgSecret, s.JID, insert.Chat.ToNonAD(), insert.Sender.ToNonAD(), insert.ID, insert.Secret)
}
err = tx.Commit()
if err != nil {
@@ -727,3 +727,42 @@ func (s *SQLStore) GetMessageSecret(chat, sender types.JID, id types.MessageID)
}
return
}
const (
putPrivacyTokens = `
INSERT INTO whatsmeow_privacy_tokens (our_jid, their_jid, token, timestamp)
VALUES ($1, $2, $3, $4)
ON CONFLICT (our_jid, their_jid) DO UPDATE SET token=EXCLUDED.token, timestamp=EXCLUDED.timestamp
`
getPrivacyToken = `SELECT token, timestamp FROM whatsmeow_privacy_tokens WHERE our_jid=$1 AND their_jid=$2`
)
func (s *SQLStore) PutPrivacyTokens(tokens ...store.PrivacyToken) error {
args := make([]any, 1+len(tokens)*3)
placeholders := make([]string, len(tokens))
args[0] = s.JID
for i, token := range tokens {
args[i*3+1] = token.User.ToNonAD().String()
args[i*3+2] = token.Token
args[i*3+3] = token.Timestamp.Unix()
placeholders[i] = fmt.Sprintf("($1, $%d, $%d, $%d)", i*3+2, i*3+3, i*3+4)
}
query := strings.ReplaceAll(putPrivacyTokens, "($1, $2, $3, $4)", strings.Join(placeholders, ","))
_, err := s.db.Exec(query, args...)
return err
}
func (s *SQLStore) GetPrivacyToken(user types.JID) (*store.PrivacyToken, error) {
var token store.PrivacyToken
token.User = user.ToNonAD()
var ts int64
err := s.db.QueryRow(getPrivacyToken, s.JID, token.User).Scan(&token.Token, &ts)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else if err != nil {
return nil, err
} else {
token.Timestamp = time.Unix(ts, 0)
return &token, nil
}
}
+12 -1
View File
@@ -16,7 +16,7 @@ type upgradeFunc func(*sql.Tx, *Container) error
//
// This may be of use if you want to manage the database fully manually, but in most cases you
// should just call Container.Upgrade to let the library handle everything.
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2, upgradeV3}
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2, upgradeV3, upgradeV4}
func (c *Container) getVersion() (int, error) {
_, err := c.db.Exec("CREATE TABLE IF NOT EXISTS whatsmeow_version (version INTEGER)")
@@ -260,3 +260,14 @@ func upgradeV3(tx *sql.Tx, container *Container) error {
)`)
return err
}
func upgradeV4(tx *sql.Tx, container *Container) error {
_, err := tx.Exec(`CREATE TABLE whatsmeow_privacy_tokens (
our_jid TEXT,
their_jid TEXT,
token bytea NOT NULL,
timestamp BIGINT NOT NULL,
PRIMARY KEY (our_jid, their_jid)
)`)
return err
}
+23 -11
View File
@@ -112,6 +112,17 @@ type MsgSecretStore interface {
GetMessageSecret(chat, sender types.JID, id types.MessageID) ([]byte, error)
}
type PrivacyToken struct {
User types.JID
Token []byte
Timestamp time.Time
}
type PrivacyTokenStore interface {
PutPrivacyTokens(tokens ...PrivacyToken) error
GetPrivacyToken(user types.JID) (*PrivacyToken, error)
}
type Device struct {
Log waLog.Logger
@@ -127,17 +138,18 @@ type Device struct {
BusinessName string
PushName string
Initialized bool
Identities IdentityStore
Sessions SessionStore
PreKeys PreKeyStore
SenderKeys SenderKeyStore
AppStateKeys AppStateSyncKeyStore
AppState AppStateStore
Contacts ContactStore
ChatSettings ChatSettingsStore
MsgSecrets MsgSecretStore
Container DeviceContainer
Initialized bool
Identities IdentityStore
Sessions SessionStore
PreKeys PreKeyStore
SenderKeys SenderKeyStore
AppStateKeys AppStateSyncKeyStore
AppState AppStateStore
Contacts ContactStore
ChatSettings ChatSettingsStore
MsgSecrets MsgSecretStore
PrivacyTokens PrivacyTokenStore
Container DeviceContainer
DatabaseErrorHandler func(device *Device, action string, attemptIndex int, err error) (retry bool)
}
+11 -6
View File
@@ -92,16 +92,16 @@ type StreamReplaced struct{}
type TempBanReason int
const (
TempBanBlockedByUsers TempBanReason = 101
TempBanSentToTooManyPeople TempBanReason = 102
TempBanSentToTooManyPeople TempBanReason = 101
TempBanBlockedByUsers TempBanReason = 102
TempBanCreatedTooManyGroups TempBanReason = 103
TempBanSentTooManySameMessage TempBanReason = 104
TempBanBroadcastList TempBanReason = 106
)
var tempBanReasonMessage = map[TempBanReason]string{
TempBanBlockedByUsers: "too many people blocked you",
TempBanSentToTooManyPeople: "you sent too many messages to people who don't have you in their address books",
TempBanBlockedByUsers: "too many people blocked you",
TempBanCreatedTooManyGroups: "you created too many groups with people who don't have you in their address books",
TempBanSentTooManySameMessage: "you sent the same message to too many people",
TempBanBroadcastList: "you sent too many messages to a broadcast list",
@@ -119,14 +119,14 @@ func (tbr TempBanReason) String() string {
// TemporaryBan is emitted when there's a connection failure with the ConnectFailureTempBanned reason code.
type TemporaryBan struct {
Code TempBanReason
Expire time.Time
Expire time.Duration
}
func (tb *TemporaryBan) String() string {
if tb.Expire.IsZero() {
if tb.Expire == 0 {
return fmt.Sprintf("You've been temporarily banned: %v", tb.Code)
}
return fmt.Sprintf("You've been temporarily banned: %v. The ban expires at %v", tb.Code, tb.Expire)
return fmt.Sprintf("You've been temporarily banned: %v. The ban expires in %v", tb.Code, tb.Expire)
}
// ConnectFailureReason is an error code included in connection failure events.
@@ -352,6 +352,11 @@ type GroupInfo struct {
Announce *types.GroupAnnounce // Group announce status change (can only admins send messages?)
Ephemeral *types.GroupEphemeral // Disappearing messages change
Delete *types.GroupDelete
Link *types.GroupLinkChange
Unlink *types.GroupLinkChange
NewInviteLink *string // Group invite link change
PrevParticipantVersionID string
+59
View File
@@ -27,6 +27,10 @@ type GroupInfo struct {
GroupAnnounce
GroupEphemeral
GroupParent
GroupLinkedParent
GroupIsDefaultSub
GroupCreated time.Time
ParticipantVersionID string
@@ -35,6 +39,19 @@ type GroupInfo struct {
MemberAddMode GroupMemberAddMode
}
type GroupParent struct {
IsParent bool
DefaultMembershipApprovalMode string // request_required
}
type GroupLinkedParent struct {
LinkedParentJID JID
}
type GroupIsDefaultSub struct {
IsDefaultSubGroup bool
}
// GroupName contains the name of a group along with metadata of who set it and when.
type GroupName struct {
Name string
@@ -67,6 +84,16 @@ type GroupParticipant struct {
JID JID
IsAdmin bool
IsSuperAdmin bool
// When creating groups, adding some participants may fail.
// In such cases, the error code will be here.
Error int
AddRequest *GroupParticipantAddRequest
}
type GroupParticipantAddRequest struct {
Code string
Expiration time.Time
}
// GroupEphemeral contains the group's disappearing messages settings.
@@ -74,3 +101,35 @@ type GroupEphemeral struct {
IsEphemeral bool
DisappearingTimer uint32
}
type GroupDelete struct {
Deleted bool
DeleteReason string
}
type GroupLinkChangeType string
const (
GroupLinkChangeTypeParent GroupLinkChangeType = "parent_group"
GroupLinkChangeTypeSub GroupLinkChangeType = "sub_group"
GroupLinkChangeTypeSibling GroupLinkChangeType = "sibling_group"
)
type GroupUnlinkReason string
const (
GroupUnlinkReasonDefault GroupUnlinkReason = "unlink_group"
GroupUnlinkReasonDelete GroupUnlinkReason = "delete_parent"
)
type GroupLinkTarget struct {
JID JID
GroupName
GroupIsDefaultSub
}
type GroupLinkChange struct {
Type GroupLinkChangeType
UnlinkReason GroupUnlinkReason
Group GroupLinkTarget
}
+1 -1
View File
@@ -54,7 +54,7 @@ type UploadResponse struct {
// FileSha256: resp.FileSha256,
// FileLength: &resp.FileLength,
// }
// _, err = cli.SendMessage(context.Background(), targetJID, "", &waProto.Message{
// _, err = cli.SendMessage(context.Background(), targetJID, &waProto.Message{
// ImageMessage: imageMsg,
// })
// // handle error again
+32 -8
View File
@@ -270,29 +270,53 @@ func (cli *Client) GetUserDevicesContext(ctx context.Context, jids []types.JID)
return devices, nil
}
type GetProfilePictureParams struct {
Preview bool
ExistingID string
IsCommunity bool
}
// GetProfilePictureInfo gets the URL where you can download a WhatsApp user's profile picture or group's photo.
//
// Optionally, you can pass the last known profile picture ID.
// If the profile picture hasn't changed, this will return nil with no error.
func (cli *Client) GetProfilePictureInfo(jid types.JID, preview bool, existingID string) (*types.ProfilePictureInfo, error) {
//
// To get a community photo, you should pass `IsCommunity: true`, as otherwise you may get a 401 error.
func (cli *Client) GetProfilePictureInfo(jid types.JID, params *GetProfilePictureParams) (*types.ProfilePictureInfo, error) {
attrs := waBinary.Attrs{
"query": "url",
}
if preview {
if params == nil {
params = &GetProfilePictureParams{}
}
if params.Preview {
attrs["type"] = "preview"
} else {
attrs["type"] = "image"
}
if existingID != "" {
attrs["id"] = existingID
if params.ExistingID != "" {
attrs["id"] = params.ExistingID
}
var pictureContent []waBinary.Node
namespace := "w:profile:picture"
if params.IsCommunity {
namespace = "w:g2"
pictureContent = []waBinary.Node{{
Tag: "query_linked",
Attrs: waBinary.Attrs{
"type": "parent_group",
"jid": jid,
},
}}
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:profile:picture",
Namespace: namespace,
Type: "get",
To: jid,
Content: []waBinary.Node{{
Tag: "picture",
Attrs: attrs,
Tag: "picture",
Attrs: attrs,
Content: pictureContent,
}},
})
if errors.Is(err, ErrIQNotAuthorized) {
@@ -304,7 +328,7 @@ func (cli *Client) GetProfilePictureInfo(jid types.JID, preview bool, existingID
}
picture, ok := resp.GetOptionalChildByTag("picture")
if !ok {
if existingID != "" {
if params.ExistingID != "" {
return nil, nil
}
return nil, &ElementMissingError{Tag: "picture", In: "response to profile picture query"}