Update dependencies (#1951)
This commit is contained in:
+3
-2
@@ -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)
|
||||
}
|
||||
|
||||
+878
-776
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+22
-3
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Vendored
+50
-6
@@ -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 {
|
||||
|
||||
Vendored
+172
-13
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
+27
-23
@@ -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
@@ -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,
|
||||
|
||||
Vendored
+9
-4
@@ -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() {
|
||||
|
||||
Vendored
+5
-1
@@ -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)
|
||||
|
||||
Vendored
+76
-41
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+32
-8
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user