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

Add dependencies/vendor (whatsapp)

This commit is contained in:
Wim
2022-01-31 00:27:37 +01:00
parent e7b193788a
commit e3cafeaf92
1074 changed files with 3091569 additions and 26075 deletions

374
vendor/go.mau.fi/whatsmeow/LICENSE vendored Normal file
View File

@ -0,0 +1,374 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

39
vendor/go.mau.fi/whatsmeow/README.md vendored Normal file
View File

@ -0,0 +1,39 @@
# whatsmeow
[![godocs.io](https://godocs.io/go.mau.fi/whatsmeow?status.svg)](https://godocs.io/go.mau.fi/whatsmeow)
whatsmeow is a Go library for the WhatsApp web multidevice API.
This was initially forked from [go-whatsapp] (MIT license), but large parts of
the code have been rewritten for multidevice support. Parts of the code are
ported from [WhatsappWeb4j] and [Baileys] (also MIT license).
[go-whatsapp]: https://github.com/Rhymen/go-whatsapp
[WhatsappWeb4j]: https://github.com/Auties00/WhatsappWeb4j
[Baileys]: https://github.com/adiwajshing/Baileys
## Discussion
Matrix room: [#whatsmeow:maunium.net](https://matrix.to/#/#whatsmeow:maunium.net)
## Usage
The [godoc](https://godocs.io/go.mau.fi/whatsmeow) includes docs for all methods and event types.
There's also a [simple example](https://godocs.io/go.mau.fi/whatsmeow#example-package) at the top.
Also see [mdtest](./mdtest) for a CLI tool you can easily try out whatsmeow with.
## Features
Most core features are already present:
* Sending messages to private chats and groups (both text and media)
* Receiving all messages
* Managing groups and receiving group change events
* Joining via invite messages, using and creating invite links
* Sending and receiving typing notifications
* Sending and receiving delivery and read receipts
* Reading app state (contact list, chat pin/mute status, etc)
* Sending and handling retry receipts if message decryption fails
Things that are not yet implemented:
* Writing app state (contact list, chat pin/mute status, etc)
* Sending status messages or broadcast list messages (this is not supported on WhatsApp web either)
* Calls

193
vendor/go.mau.fi/whatsmeow/appstate.go vendored Normal file
View File

@ -0,0 +1,193 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"fmt"
"time"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
// FetchAppState fetches updates to the given type of app state. If fullSync is true, the current
// cached state will be removed and all app state patches will be re-fetched from the server.
func (cli *Client) FetchAppState(name appstate.WAPatchName, fullSync, onlyIfNotSynced bool) error {
cli.appStateSyncLock.Lock()
defer cli.appStateSyncLock.Unlock()
if fullSync {
err := cli.Store.AppState.DeleteAppStateVersion(string(name))
if err != nil {
return fmt.Errorf("failed to reset app state %s version: %w", name, err)
}
}
version, hash, err := cli.Store.AppState.GetAppStateVersion(string(name))
if err != nil {
return fmt.Errorf("failed to get app state %s version: %w", name, err)
}
if version == 0 {
fullSync = true
} else if onlyIfNotSynced {
return nil
}
state := appstate.HashState{Version: version, Hash: hash}
hasMore := true
wantSnapshot := fullSync
for hasMore {
patches, err := cli.fetchAppStatePatches(name, state.Version, wantSnapshot)
wantSnapshot = false
if err != nil {
return fmt.Errorf("failed to fetch app state %s patches: %w", name, err)
}
hasMore = patches.HasMorePatches
mutations, newState, err := cli.appStateProc.DecodePatches(patches, state, true)
if err != nil {
return fmt.Errorf("failed to decode app state %s patches: %w", name, err)
}
state = newState
for _, mutation := range mutations {
cli.dispatchAppState(mutation, !fullSync || cli.EmitAppStateEventsOnFullSync)
}
}
if fullSync {
cli.Log.Debugf("Full sync of app state %s completed. Current version: %d", name, state.Version)
cli.dispatchEvent(&events.AppStateSyncComplete{Name: name})
} else {
cli.Log.Debugf("Synced app state %s from version %d to %d", name, version, state.Version)
}
return nil
}
func (cli *Client) dispatchAppState(mutation appstate.Mutation, dispatchEvts bool) {
if mutation.Operation != waProto.SyncdMutation_SET {
return
}
if dispatchEvts {
cli.dispatchEvent(&events.AppState{Index: mutation.Index, SyncActionValue: mutation.Action})
}
var jid types.JID
if len(mutation.Index) > 1 {
jid, _ = types.ParseJID(mutation.Index[1])
}
ts := time.Unix(mutation.Action.GetTimestamp(), 0)
var storeUpdateError error
var eventToDispatch interface{}
switch mutation.Index[0] {
case "mute":
act := mutation.Action.GetMuteAction()
eventToDispatch = &events.Mute{JID: jid, Timestamp: ts, Action: act}
var mutedUntil time.Time
if act.GetMuted() {
mutedUntil = time.Unix(act.GetMuteEndTimestamp(), 0)
}
if cli.Store.ChatSettings != nil {
storeUpdateError = cli.Store.ChatSettings.PutMutedUntil(jid, mutedUntil)
}
case "pin_v1":
act := mutation.Action.GetPinAction()
eventToDispatch = &events.Pin{JID: jid, Timestamp: ts, Action: act}
if cli.Store.ChatSettings != nil {
storeUpdateError = cli.Store.ChatSettings.PutPinned(jid, act.GetPinned())
}
case "archive":
act := mutation.Action.GetArchiveChatAction()
eventToDispatch = &events.Archive{JID: jid, Timestamp: ts, Action: act}
if cli.Store.ChatSettings != nil {
storeUpdateError = cli.Store.ChatSettings.PutArchived(jid, act.GetArchived())
}
case "contact":
act := mutation.Action.GetContactAction()
eventToDispatch = &events.Contact{JID: jid, Timestamp: ts, Action: act}
if cli.Store.Contacts != nil {
storeUpdateError = cli.Store.Contacts.PutContactName(jid, act.GetFirstName(), act.GetFullName())
}
case "star":
if len(mutation.Index) < 5 {
return
}
evt := events.Star{
ChatJID: jid,
MessageID: mutation.Index[2],
Timestamp: ts,
Action: mutation.Action.GetStarAction(),
IsFromMe: mutation.Index[3] == "1",
}
if mutation.Index[4] != "0" {
evt.SenderJID, _ = types.ParseJID(mutation.Index[4])
}
eventToDispatch = &evt
case "deleteMessageForMe":
if len(mutation.Index) < 5 {
return
}
evt := events.DeleteForMe{
ChatJID: jid,
MessageID: mutation.Index[2],
Timestamp: ts,
Action: mutation.Action.GetDeleteMessageForMeAction(),
IsFromMe: mutation.Index[3] == "1",
}
if mutation.Index[4] != "0" {
evt.SenderJID, _ = types.ParseJID(mutation.Index[4])
}
eventToDispatch = &evt
case "setting_pushName":
eventToDispatch = &events.PushNameSetting{Timestamp: ts, Action: mutation.Action.GetPushNameSetting()}
cli.Store.PushName = mutation.Action.GetPushNameSetting().GetName()
err := cli.Store.Save()
if err != nil {
cli.Log.Errorf("Failed to save device store after updating push name: %v", err)
}
case "setting_unarchiveChats":
eventToDispatch = &events.UnarchiveChatsSetting{Timestamp: ts, Action: mutation.Action.GetUnarchiveChatsSetting()}
}
if storeUpdateError != nil {
cli.Log.Errorf("Failed to update device store after app state mutation: %v", storeUpdateError)
}
if dispatchEvts && eventToDispatch != nil {
cli.dispatchEvent(eventToDispatch)
}
}
func (cli *Client) downloadExternalAppStateBlob(ref *waProto.ExternalBlobReference) ([]byte, error) {
return cli.Download(ref)
}
func (cli *Client) fetchAppStatePatches(name appstate.WAPatchName, fromVersion uint64, snapshot bool) (*appstate.PatchList, error) {
attrs := waBinary.Attrs{
"name": string(name),
"return_snapshot": snapshot,
}
if !snapshot {
attrs["version"] = fromVersion
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:sync:app:state",
Type: "set",
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "sync",
Content: []waBinary.Node{{
Tag: "collection",
Attrs: attrs,
}},
}},
})
if err != nil {
return nil, err
}
return appstate.ParsePatchList(resp, cli.downloadExternalAppStateBlob)
}

View File

@ -0,0 +1,310 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package appstate
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/util/cbcutil"
)
// PatchList represents a decoded response to getting app state patches from the WhatsApp servers.
type PatchList struct {
Name WAPatchName
HasMorePatches bool
Patches []*waProto.SyncdPatch
Snapshot *waProto.SyncdSnapshot
}
// DownloadExternalFunc is a function that can download a blob of external app state patches.
type DownloadExternalFunc func(*waProto.ExternalBlobReference) ([]byte, error)
func parseSnapshotInternal(collection *waBinary.Node, downloadExternal DownloadExternalFunc) (*waProto.SyncdSnapshot, error) {
snapshotNode := collection.GetChildByTag("snapshot")
rawSnapshot, ok := snapshotNode.Content.([]byte)
if snapshotNode.Tag != "snapshot" || !ok {
return nil, nil
}
var snapshot waProto.ExternalBlobReference
err := proto.Unmarshal(rawSnapshot, &snapshot)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal snapshot: %w", err)
}
var rawData []byte
rawData, err = downloadExternal(&snapshot)
if err != nil {
return nil, fmt.Errorf("failed to download external mutations: %w", err)
}
var downloaded waProto.SyncdSnapshot
err = proto.Unmarshal(rawData, &downloaded)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal mutation list: %w", err)
}
return &downloaded, nil
}
func parsePatchListInternal(collection *waBinary.Node, downloadExternal DownloadExternalFunc) ([]*waProto.SyncdPatch, error) {
patchesNode := collection.GetChildByTag("patches")
patchNodes := patchesNode.GetChildren()
patches := make([]*waProto.SyncdPatch, 0, len(patchNodes))
for i, patchNode := range patchNodes {
rawPatch, ok := patchNode.Content.([]byte)
if patchNode.Tag != "patch" || !ok {
continue
}
var patch waProto.SyncdPatch
err := proto.Unmarshal(rawPatch, &patch)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal patch #%d: %w", i+1, err)
}
if patch.GetExternalMutations() != nil && downloadExternal != nil {
var rawData []byte
rawData, err = downloadExternal(patch.GetExternalMutations())
if err != nil {
return nil, fmt.Errorf("failed to download external mutations: %w", err)
}
var downloaded waProto.SyncdMutations
err = proto.Unmarshal(rawData, &downloaded)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal mutation list: %w", err)
} else if len(downloaded.GetMutations()) == 0 {
return nil, fmt.Errorf("didn't get any mutations from download")
}
patch.Mutations = downloaded.Mutations
}
patches = append(patches, &patch)
}
return patches, nil
}
// ParsePatchList will decode an XML node containing app state patches, including downloading any external blobs.
func ParsePatchList(node *waBinary.Node, downloadExternal DownloadExternalFunc) (*PatchList, error) {
collection := node.GetChildByTag("sync", "collection")
ag := collection.AttrGetter()
snapshot, err := parseSnapshotInternal(&collection, downloadExternal)
if err != nil {
return nil, err
}
patches, err := parsePatchListInternal(&collection, downloadExternal)
if err != nil {
return nil, err
}
list := &PatchList{
Name: WAPatchName(ag.String("name")),
HasMorePatches: ag.OptionalBool("has_more_patches"),
Patches: patches,
Snapshot: snapshot,
}
return list, ag.Error()
}
type patchOutput struct {
RemovedMACs [][]byte
AddedMACs []store.AppStateMutationMAC
Mutations []Mutation
}
func (proc *Processor) decodeMutations(mutations []*waProto.SyncdMutation, out *patchOutput, validateMACs bool) error {
for i, mutation := range mutations {
keyID := mutation.GetRecord().GetKeyId().GetId()
keys, err := proc.getAppStateKey(keyID)
if err != nil {
return fmt.Errorf("failed to get key %X to decode mutation: %w", keyID, err)
}
content := mutation.GetRecord().GetValue().GetBlob()
content, valueMAC := content[:len(content)-32], content[len(content)-32:]
if validateMACs {
expectedValueMAC := generateContentMAC(mutation.GetOperation(), content, keyID, keys.ValueMAC)
if !bytes.Equal(expectedValueMAC, valueMAC) {
return fmt.Errorf("failed to verify mutation #%d: %w", i+1, ErrMismatchingContentMAC)
}
}
iv, content := content[:16], content[16:]
plaintext, err := cbcutil.Decrypt(keys.ValueEncryption, iv, content)
if err != nil {
return fmt.Errorf("failed to decrypt mutation #%d: %w", i+1, err)
}
var syncAction waProto.SyncActionData
err = proto.Unmarshal(plaintext, &syncAction)
if err != nil {
return fmt.Errorf("failed to unmarshal mutation #%d: %w", i+1, err)
}
indexMAC := mutation.GetRecord().GetIndex().GetBlob()
if validateMACs {
expectedIndexMAC := concatAndHMAC(sha256.New, keys.Index, syncAction.Index)
if !bytes.Equal(expectedIndexMAC, indexMAC) {
return fmt.Errorf("failed to verify mutation #%d: %w", i+1, ErrMismatchingIndexMAC)
}
}
var index []string
err = json.Unmarshal(syncAction.GetIndex(), &index)
if err != nil {
return fmt.Errorf("failed to unmarshal index of mutation #%d: %w", i+1, err)
}
if mutation.GetOperation() == waProto.SyncdMutation_REMOVE {
out.RemovedMACs = append(out.RemovedMACs, indexMAC)
} else if mutation.GetOperation() == waProto.SyncdMutation_SET {
out.AddedMACs = append(out.AddedMACs, store.AppStateMutationMAC{
IndexMAC: indexMAC,
ValueMAC: valueMAC,
})
}
out.Mutations = append(out.Mutations, Mutation{
Operation: mutation.GetOperation(),
Action: syncAction.GetValue(),
Index: index,
IndexMAC: indexMAC,
ValueMAC: valueMAC,
})
}
return nil
}
func (proc *Processor) storeMACs(name WAPatchName, currentState HashState, out *patchOutput) {
err := proc.Store.AppState.PutAppStateVersion(string(name), currentState.Version, currentState.Hash)
if err != nil {
proc.Log.Errorf("Failed to update app state version in the database: %v", err)
}
err = proc.Store.AppState.DeleteAppStateMutationMACs(string(name), out.RemovedMACs)
if err != nil {
proc.Log.Errorf("Failed to remove deleted mutation MACs from the database: %v", err)
}
err = proc.Store.AppState.PutAppStateMutationMACs(string(name), currentState.Version, out.AddedMACs)
if err != nil {
proc.Log.Errorf("Failed to insert added mutation MACs to the database: %v", err)
}
}
func (proc *Processor) validateSnapshotMAC(name WAPatchName, currentState HashState, keyID, expectedSnapshotMAC []byte) (keys ExpandedAppStateKeys, err error) {
keys, err = proc.getAppStateKey(keyID)
if err != nil {
err = fmt.Errorf("failed to get key %X to verify patch v%d MACs: %w", keyID, currentState.Version, err)
return
}
snapshotMAC := currentState.generateSnapshotMAC(name, keys.SnapshotMAC)
if !bytes.Equal(snapshotMAC, expectedSnapshotMAC) {
err = fmt.Errorf("failed to verify patch v%d: %w", currentState.Version, ErrMismatchingLTHash)
}
return
}
func (proc *Processor) decodeSnapshot(name WAPatchName, ss *waProto.SyncdSnapshot, initialState HashState, validateMACs bool, newMutationsInput []Mutation) (newMutations []Mutation, currentState HashState, err error) {
currentState = initialState
currentState.Version = ss.GetVersion().GetVersion()
encryptedMutations := make([]*waProto.SyncdMutation, len(ss.GetRecords()))
for i, record := range ss.GetRecords() {
encryptedMutations[i] = &waProto.SyncdMutation{
Operation: waProto.SyncdMutation_SET.Enum(),
Record: record,
}
}
var warn []error
warn, err = currentState.updateHash(encryptedMutations, func(indexMAC []byte, maxIndex int) ([]byte, error) {
return nil, nil
})
if len(warn) > 0 {
proc.Log.Warnf("Warnings while updating hash for %s: %+v", name, warn)
}
if err != nil {
err = fmt.Errorf("failed to update state hash: %w", err)
return
}
if validateMACs {
_, err = proc.validateSnapshotMAC(name, currentState, ss.GetKeyId().GetId(), ss.GetMac())
if err != nil {
return
}
}
var out patchOutput
out.Mutations = newMutationsInput
err = proc.decodeMutations(encryptedMutations, &out, validateMACs)
if err != nil {
err = fmt.Errorf("failed to decode snapshot of v%d: %w", currentState.Version, err)
return
}
proc.storeMACs(name, currentState, &out)
newMutations = out.Mutations
return
}
// DecodePatches will decode all the patches in a PatchList into a list of app state mutations.
func (proc *Processor) DecodePatches(list *PatchList, initialState HashState, validateMACs bool) (newMutations []Mutation, currentState HashState, err error) {
currentState = initialState
var expectedLength int
if list.Snapshot != nil {
expectedLength = len(list.Snapshot.GetRecords())
}
for _, patch := range list.Patches {
expectedLength += len(patch.GetMutations())
}
newMutations = make([]Mutation, 0, expectedLength)
if list.Snapshot != nil {
newMutations, currentState, err = proc.decodeSnapshot(list.Name, list.Snapshot, currentState, validateMACs, newMutations)
if err != nil {
return
}
}
for _, patch := range list.Patches {
version := patch.GetVersion().GetVersion()
currentState.Version = version
var warn []error
warn, err = currentState.updateHash(patch.GetMutations(), func(indexMAC []byte, maxIndex int) ([]byte, error) {
for i := maxIndex - 1; i >= 0; i-- {
if bytes.Equal(patch.Mutations[i].GetRecord().GetIndex().GetBlob(), indexMAC) {
value := patch.Mutations[i].GetRecord().GetValue().GetBlob()
return value[len(value)-32:], nil
}
}
// Previous value not found in current patch, look in the database
return proc.Store.AppState.GetAppStateMutationMAC(string(list.Name), indexMAC)
})
if len(warn) > 0 {
proc.Log.Warnf("Warnings while updating hash for %s: %+v", list.Name, warn)
}
if err != nil {
err = fmt.Errorf("failed to update state hash: %w", err)
return
}
if validateMACs {
var keys ExpandedAppStateKeys
keys, err = proc.validateSnapshotMAC(list.Name, currentState, patch.GetKeyId().GetId(), patch.GetSnapshotMac())
if err != nil {
return
}
patchMAC := generatePatchMAC(patch, list.Name, keys.PatchMAC)
if !bytes.Equal(patchMAC, patch.GetPatchMac()) {
err = fmt.Errorf("failed to verify patch v%d: %w", version, ErrMismatchingPatchMAC)
return
}
}
var out patchOutput
out.Mutations = newMutations
err = proc.decodeMutations(patch.GetMutations(), &out, validateMACs)
if err != nil {
return
}
proc.storeMACs(list.Name, currentState, &out)
newMutations = out.Mutations
}
return
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package appstate
import "errors"
// Errors that this package can return.
var (
ErrMissingPreviousSetValueOperation = errors.New("missing value MAC of previous SET operation")
ErrMismatchingLTHash = errors.New("mismatching LTHash")
ErrMismatchingPatchMAC = errors.New("mismatching patch MAC")
ErrMismatchingContentMAC = errors.New("mismatching content MAC")
ErrMismatchingIndexMAC = errors.New("mismatching index MAC")
ErrKeyNotFound = errors.New("didn't find app state key")
)

View File

@ -0,0 +1,96 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package appstate
import (
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"fmt"
"hash"
"go.mau.fi/whatsmeow/appstate/lthash"
waProto "go.mau.fi/whatsmeow/binary/proto"
)
type Mutation struct {
Operation waProto.SyncdMutation_SyncdMutationSyncdOperation
Action *waProto.SyncActionValue
Index []string
IndexMAC []byte
ValueMAC []byte
}
type HashState struct {
Version uint64
Hash [128]byte
}
func (hs *HashState) updateHash(mutations []*waProto.SyncdMutation, getPrevSetValueMAC func(indexMAC []byte, maxIndex int) ([]byte, error)) ([]error, error) {
var added, removed [][]byte
var warnings []error
for i, mutation := range mutations {
if mutation.GetOperation() == waProto.SyncdMutation_SET {
value := mutation.GetRecord().GetValue().GetBlob()
added = append(added, value[len(value)-32:])
}
indexMAC := mutation.GetRecord().GetIndex().GetBlob()
removal, err := getPrevSetValueMAC(indexMAC, i)
if err != nil {
return warnings, fmt.Errorf("failed to get value MAC of previous SET operation: %w", err)
} else if removal != nil {
removed = append(removed, removal)
} else if mutation.GetOperation() == waProto.SyncdMutation_REMOVE {
// TODO figure out if there are certain cases that are safe to ignore and others that aren't
// At least removing contact access from WhatsApp seems to create a REMOVE op for your own JID
// that points to a non-existent index and is safe to ignore here. Other keys might not be safe to ignore.
warnings = append(warnings, fmt.Errorf("%w for %X", ErrMissingPreviousSetValueOperation, indexMAC))
//return ErrMissingPreviousSetValueOperation
}
}
lthash.WAPatchIntegrity.SubtractThenAddInPlace(hs.Hash[:], removed, added)
return warnings, nil
}
func uint64ToBytes(val uint64) []byte {
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, val)
return data
}
func concatAndHMAC(alg func() hash.Hash, key []byte, data ...[]byte) []byte {
h := hmac.New(alg, key)
for _, item := range data {
h.Write(item)
}
return h.Sum(nil)
}
func (hs *HashState) generateSnapshotMAC(name WAPatchName, key []byte) []byte {
return concatAndHMAC(sha256.New, key, hs.Hash[:], uint64ToBytes(hs.Version), []byte(name))
}
func generatePatchMAC(patch *waProto.SyncdPatch, name WAPatchName, key []byte) []byte {
dataToHash := make([][]byte, len(patch.GetMutations())+3)
dataToHash[0] = patch.GetSnapshotMac()
for i, mutation := range patch.Mutations {
val := mutation.GetRecord().GetValue().GetBlob()
dataToHash[i+1] = val[len(val)-32:]
}
dataToHash[len(dataToHash)-2] = uint64ToBytes(patch.GetVersion().GetVersion())
dataToHash[len(dataToHash)-1] = []byte(name)
return concatAndHMAC(sha256.New, key, dataToHash...)
}
func generateContentMAC(operation waProto.SyncdMutation_SyncdMutationSyncdOperation, data, keyID, key []byte) []byte {
operationBytes := []byte{byte(operation) + 1}
keyDataLength := uint64ToBytes(uint64(len(keyID) + 1))
return concatAndHMAC(sha512.New, key, operationBytes, keyID, data, keyDataLength)[:32]
}

View File

@ -0,0 +1,85 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package appstate implements encoding and decoding WhatsApp's app state patches.
package appstate
import (
"encoding/base64"
"sync"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/util/hkdfutil"
waLog "go.mau.fi/whatsmeow/util/log"
)
// WAPatchName represents a type of app state patch.
type WAPatchName string
const (
// WAPatchCriticalBlock contains the user's settings like push name and locale.
WAPatchCriticalBlock WAPatchName = "critical_block"
// WAPatchCriticalUnblockLow contains the user's contact list.
WAPatchCriticalUnblockLow WAPatchName = "critical_unblock_low"
// WAPatchRegularLow contains some local chat settings like pin, archive status, and the setting of whether to unarchive chats when messages come in.
WAPatchRegularLow WAPatchName = "regular_low"
// WAPatchRegularHigh contains more local chat settings like mute status and starred messages.
WAPatchRegularHigh WAPatchName = "regular_high"
// WAPatchRegular contains protocol info about app state patches like key expiration.
WAPatchRegular WAPatchName = "regular"
)
// AllPatchNames contains all currently known patch state names.
var AllPatchNames = [...]WAPatchName{WAPatchCriticalBlock, WAPatchCriticalUnblockLow, WAPatchRegularHigh, WAPatchRegular, WAPatchRegularLow}
type Processor struct {
keyCache map[string]ExpandedAppStateKeys
keyCacheLock sync.Mutex
Store *store.Device
Log waLog.Logger
}
func NewProcessor(store *store.Device, log waLog.Logger) *Processor {
return &Processor{
keyCache: make(map[string]ExpandedAppStateKeys),
Store: store,
Log: log,
}
}
type ExpandedAppStateKeys struct {
Index []byte
ValueEncryption []byte
ValueMAC []byte
SnapshotMAC []byte
PatchMAC []byte
}
func expandAppStateKeys(keyData []byte) (keys ExpandedAppStateKeys) {
appStateKeyExpanded := hkdfutil.SHA256(keyData, nil, []byte("WhatsApp Mutation Keys"), 160)
return ExpandedAppStateKeys{appStateKeyExpanded[0:32], appStateKeyExpanded[32:64], appStateKeyExpanded[64:96], appStateKeyExpanded[96:128], appStateKeyExpanded[128:160]}
}
func (proc *Processor) getAppStateKey(keyID []byte) (keys ExpandedAppStateKeys, err error) {
keyCacheID := base64.RawStdEncoding.EncodeToString(keyID)
var ok bool
proc.keyCacheLock.Lock()
defer proc.keyCacheLock.Unlock()
keys, ok = proc.keyCache[keyCacheID]
if !ok {
var keyData *store.AppStateSyncKey
keyData, err = proc.Store.AppStateKeys.GetAppStateSyncKey(keyID)
if keyData != nil {
keys = expandAppStateKeys(keyData.Data)
proc.keyCache[keyCacheID] = keys
} else if err == nil {
err = ErrKeyNotFound
}
}
return
}

View File

@ -0,0 +1,58 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package lthash implements a summation based hash algorithm that maintains the
// integrity of a piece of data over a series of mutations. You can add/remove
// mutations, and it'll return a hash equal to if the same series of mutations
// was made sequentially.
package lthash
import (
"encoding/binary"
"go.mau.fi/whatsmeow/util/hkdfutil"
)
type LTHash struct {
HKDFInfo []byte
HKDFSize uint8
}
// WAPatchIntegrity is a LTHash instance initialized with the details used for verifying integrity of WhatsApp app state sync patches.
var WAPatchIntegrity = LTHash{[]byte("WhatsApp Patch Integrity"), 128}
func (lth LTHash) SubtractThenAdd(base []byte, subtract, add [][]byte) []byte {
output := make([]byte, len(base))
copy(output, base)
lth.SubtractThenAddInPlace(output, subtract, add)
return output
}
func (lth LTHash) SubtractThenAddInPlace(base []byte, subtract, add [][]byte) {
lth.multipleOp(base, subtract, true)
lth.multipleOp(base, add, false)
}
func (lth LTHash) multipleOp(base []byte, input [][]byte, subtract bool) {
for _, item := range input {
performPointwiseWithOverflow(base, hkdfutil.SHA256(item, nil, lth.HKDFInfo, lth.HKDFSize), subtract)
}
}
func performPointwiseWithOverflow(base, input []byte, subtract bool) []byte {
for i := 0; i < len(base); i += 2 {
x := binary.LittleEndian.Uint16(base[i : i+2])
y := binary.LittleEndian.Uint16(input[i : i+2])
var result uint16
if subtract {
result = x - y
} else {
result = x + y
}
binary.LittleEndian.PutUint16(base[i:i+2], result)
}
return base
}

View File

@ -0,0 +1,177 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package binary
import (
"fmt"
"strconv"
"go.mau.fi/whatsmeow/types"
)
// AttrUtility is a helper struct for reading multiple XML attributes and checking for errors afterwards.
//
// The functions return values directly and append any decoding errors to the Errors slice. The
// slice can then be checked after all necessary attributes are read, instead of having to check
// each attribute for errors separately.
type AttrUtility struct {
Attrs Attrs
Errors []error
}
// AttrGetter returns the AttrUtility for this Node.
func (n *Node) AttrGetter() *AttrUtility {
return &AttrUtility{Attrs: n.Attrs, Errors: make([]error, 0)}
}
func (au *AttrUtility) GetJID(key string, require bool) (jidVal types.JID, ok bool) {
var val interface{}
if val, ok = au.Attrs[key]; !ok {
if require {
au.Errors = append(au.Errors, fmt.Errorf("didn't find required JID attribute '%s'", key))
}
} else if jidVal, ok = val.(types.JID); !ok {
au.Errors = append(au.Errors, fmt.Errorf("expected attribute '%s' to be JID, but was %T", key, val))
}
return
}
// OptionalJID returns the JID under the given key. If there's no valid JID under the given key, this will return nil.
// However, if the attribute is completely missing, this will not store an error.
func (au *AttrUtility) OptionalJID(key string) *types.JID {
jid, ok := au.GetJID(key, false)
if ok {
return &jid
}
return nil
}
// OptionalJIDOrEmpty returns the JID under the given key. If there's no valid JID under the given key, this will return an empty JID.
// However, if the attribute is completely missing, this will not store an error.
func (au *AttrUtility) OptionalJIDOrEmpty(key string) types.JID {
jid, ok := au.GetJID(key, false)
if ok {
return jid
}
return types.EmptyJID
}
// JID returns the JID under the given key.
// If there's no valid JID under the given key, an error will be stored and a blank JID struct will be returned.
func (au *AttrUtility) JID(key string) types.JID {
jid, _ := au.GetJID(key, true)
return jid
}
func (au *AttrUtility) GetString(key string, require bool) (strVal string, ok bool) {
var val interface{}
if val, ok = au.Attrs[key]; !ok {
if require {
au.Errors = append(au.Errors, fmt.Errorf("didn't find required attribute '%s'", key))
}
} else if strVal, ok = val.(string); !ok {
au.Errors = append(au.Errors, fmt.Errorf("expected attribute '%s' to be string, but was %T", key, val))
}
return
}
func (au *AttrUtility) GetInt64(key string, require bool) (int64, bool) {
if strVal, ok := au.GetString(key, require); !ok {
return 0, false
} else if intVal, err := strconv.ParseInt(strVal, 10, 64); err != nil {
au.Errors = append(au.Errors, fmt.Errorf("failed to parse int in attribute '%s': %w", key, err))
return 0, false
} else {
return intVal, true
}
}
func (au *AttrUtility) GetUint64(key string, require bool) (uint64, bool) {
if strVal, ok := au.GetString(key, require); !ok {
return 0, false
} else if intVal, err := strconv.ParseUint(strVal, 10, 64); err != nil {
au.Errors = append(au.Errors, fmt.Errorf("failed to parse uint in attribute '%s': %w", key, err))
return 0, false
} else {
return intVal, true
}
}
func (au *AttrUtility) GetBool(key string, require bool) (bool, bool) {
if strVal, ok := au.GetString(key, require); !ok {
return false, false
} else if boolVal, err := strconv.ParseBool(strVal); err != nil {
au.Errors = append(au.Errors, fmt.Errorf("failed to parse bool in attribute '%s': %w", key, err))
return false, false
} else {
return boolVal, true
}
}
// OptionalString returns the string under the given key.
func (au *AttrUtility) OptionalString(key string) string {
strVal, _ := au.GetString(key, false)
return strVal
}
// String returns the string under the given key.
// If there's no valid string under the given key, an error will be stored and an empty string will be returned.
func (au *AttrUtility) String(key string) string {
strVal, _ := au.GetString(key, true)
return strVal
}
func (au *AttrUtility) OptionalInt(key string) int {
val, _ := au.GetInt64(key, false)
return int(val)
}
func (au *AttrUtility) Int(key string) int {
val, _ := au.GetInt64(key, true)
return int(val)
}
func (au *AttrUtility) Int64(key string) int64 {
val, _ := au.GetInt64(key, true)
return val
}
func (au *AttrUtility) Uint64(key string) uint64 {
val, _ := au.GetUint64(key, true)
return val
}
func (au *AttrUtility) OptionalBool(key string) bool {
val, _ := au.GetBool(key, false)
return val
}
func (au *AttrUtility) Bool(key string) bool {
val, _ := au.GetBool(key, true)
return val
}
// OK returns true if there are no errors.
func (au *AttrUtility) OK() bool {
return len(au.Errors) == 0
}
// Error returns the list of errors as a single error interface, or nil if there are no errors.
func (au *AttrUtility) Error() error {
if au.OK() {
return nil
}
return ErrorList(au.Errors)
}
// ErrorList is a list of errors that implements the error interface itself.
type ErrorList []error
// Error returns all the errors in the list as a string.
func (el ErrorList) Error() string {
return fmt.Sprintf("%+v", []error(el))
}

View File

@ -0,0 +1,353 @@
package binary
import (
"fmt"
"io"
"strings"
"go.mau.fi/whatsmeow/binary/token"
"go.mau.fi/whatsmeow/types"
)
type binaryDecoder struct {
data []byte
index int
}
func newDecoder(data []byte) *binaryDecoder {
return &binaryDecoder{data, 0}
}
func (r *binaryDecoder) checkEOS(length int) error {
if r.index+length > len(r.data) {
return io.EOF
}
return nil
}
func (r *binaryDecoder) readByte() (byte, error) {
if err := r.checkEOS(1); err != nil {
return 0, err
}
b := r.data[r.index]
r.index++
return b, nil
}
func (r *binaryDecoder) readIntN(n int, littleEndian bool) (int, error) {
if err := r.checkEOS(n); err != nil {
return 0, err
}
var ret int
for i := 0; i < n; i++ {
var curShift int
if littleEndian {
curShift = i
} else {
curShift = n - i - 1
}
ret |= int(r.data[r.index+i]) << uint(curShift*8)
}
r.index += n
return ret, nil
}
func (r *binaryDecoder) readInt8(littleEndian bool) (int, error) {
return r.readIntN(1, littleEndian)
}
func (r *binaryDecoder) readInt16(littleEndian bool) (int, error) {
return r.readIntN(2, littleEndian)
}
func (r *binaryDecoder) readInt20() (int, error) {
if err := r.checkEOS(3); err != nil {
return 0, err
}
ret := ((int(r.data[r.index]) & 15) << 16) + (int(r.data[r.index+1]) << 8) + int(r.data[r.index+2])
r.index += 3
return ret, nil
}
func (r *binaryDecoder) readInt32(littleEndian bool) (int, error) {
return r.readIntN(4, littleEndian)
}
func (r *binaryDecoder) readPacked8(tag int) (string, error) {
startByte, err := r.readByte()
if err != nil {
return "", err
}
var build strings.Builder
for i := 0; i < int(startByte&127); i++ {
currByte, err := r.readByte()
if err != nil {
return "", err
}
lower, err := unpackByte(tag, currByte&0xF0>>4)
if err != nil {
return "", err
}
upper, err := unpackByte(tag, currByte&0x0F)
if err != nil {
return "", err
}
build.WriteByte(lower)
build.WriteByte(upper)
}
ret := build.String()
if startByte>>7 != 0 {
ret = ret[:len(ret)-1]
}
return ret, nil
}
func unpackByte(tag int, value byte) (byte, error) {
switch tag {
case token.Nibble8:
return unpackNibble(value)
case token.Hex8:
return unpackHex(value)
default:
return 0, fmt.Errorf("unpackByte with unknown tag %d", tag)
}
}
func unpackNibble(value byte) (byte, error) {
switch {
case value < 10:
return '0' + value, nil
case value == 10:
return '-', nil
case value == 11:
return '.', nil
case value == 15:
return 0, nil
default:
return 0, fmt.Errorf("unpackNibble with value %d", value)
}
}
func unpackHex(value byte) (byte, error) {
switch {
case value < 10:
return '0' + value, nil
case value < 16:
return 'A' + value - 10, nil
default:
return 0, fmt.Errorf("unpackHex with value %d", value)
}
}
func (r *binaryDecoder) readListSize(tag int) (int, error) {
switch tag {
case token.ListEmpty:
return 0, nil
case token.List8:
return r.readInt8(false)
case token.List16:
return r.readInt16(false)
default:
return 0, fmt.Errorf("readListSize with unknown tag %d at position %d", tag, r.index)
}
}
func (r *binaryDecoder) read(string bool) (interface{}, error) {
tagByte, err := r.readByte()
if err != nil {
return nil, err
}
tag := int(tagByte)
switch tag {
case token.ListEmpty:
return nil, nil
case token.List8, token.List16:
return r.readList(tag)
case token.Binary8:
size, err := r.readInt8(false)
if err != nil {
return nil, err
}
return r.readBytesOrString(size, string)
case token.Binary20:
size, err := r.readInt20()
if err != nil {
return nil, err
}
return r.readBytesOrString(size, string)
case token.Binary32:
size, err := r.readInt32(false)
if err != nil {
return nil, err
}
return r.readBytesOrString(size, string)
case token.Dictionary0, token.Dictionary1, token.Dictionary2, token.Dictionary3:
i, err := r.readInt8(false)
if err != nil {
return "", err
}
return token.GetDoubleToken(tag-token.Dictionary0, i)
case token.JIDPair:
return r.readJIDPair()
case token.ADJID:
return r.readADJID()
case token.Nibble8, token.Hex8:
return r.readPacked8(tag)
default:
if tag >= 1 && tag < len(token.SingleByteTokens) {
return token.SingleByteTokens[tag], nil
}
return "", fmt.Errorf("%w %d at position %d", ErrInvalidToken, tag, r.index)
}
}
func (r *binaryDecoder) readJIDPair() (interface{}, error) {
user, err := r.read(true)
if err != nil {
return nil, err
}
server, err := r.read(true)
if err != nil {
return nil, err
} else if server == nil {
return nil, ErrInvalidJIDType
} else if user == nil {
return types.NewJID("", server.(string)), nil
}
return types.NewJID(user.(string), server.(string)), nil
}
func (r *binaryDecoder) readADJID() (interface{}, error) {
agent, err := r.readByte()
if err != nil {
return nil, err
}
device, err := r.readByte()
if err != nil {
return nil, err
}
user, err := r.read(true)
if err != nil {
return nil, err
}
return types.NewADJID(user.(string), agent, device), nil
}
func (r *binaryDecoder) readAttributes(n int) (Attrs, error) {
if n == 0 {
return nil, nil
}
ret := make(Attrs)
for i := 0; i < n; i++ {
keyIfc, err := r.read(true)
if err != nil {
return nil, err
}
key, ok := keyIfc.(string)
if !ok {
return nil, fmt.Errorf("%[1]w at position %[3]d (%[2]T): %+[2]v", ErrNonStringKey, key, r.index)
}
ret[key], err = r.read(true)
if err != nil {
return nil, err
}
}
return ret, nil
}
func (r *binaryDecoder) readList(tag int) ([]Node, error) {
size, err := r.readListSize(tag)
if err != nil {
return nil, err
}
ret := make([]Node, size)
for i := 0; i < size; i++ {
n, err := r.readNode()
if err != nil {
return nil, err
}
ret[i] = *n
}
return ret, nil
}
func (r *binaryDecoder) readNode() (*Node, error) {
ret := &Node{}
size, err := r.readInt8(false)
if err != nil {
return nil, err
}
listSize, err := r.readListSize(size)
if err != nil {
return nil, err
}
rawDesc, err := r.read(true)
if err != nil {
return nil, err
}
ret.Tag = rawDesc.(string)
if listSize == 0 || ret.Tag == "" {
return nil, ErrInvalidNode
}
ret.Attrs, err = r.readAttributes((listSize - 1) >> 1)
if err != nil {
return nil, err
}
if listSize%2 == 1 {
return ret, nil
}
ret.Content, err = r.read(false)
return ret, err
}
func (r *binaryDecoder) readBytesOrString(length int, asString bool) (interface{}, error) {
data, err := r.readRaw(length)
if err != nil {
return nil, err
}
if asString {
return string(data), nil
}
return data, nil
}
func (r *binaryDecoder) readRaw(length int) ([]byte, error) {
if err := r.checkEOS(length); err != nil {
return nil, err
}
ret := r.data[r.index : r.index+length]
r.index += length
return ret, nil
}

View File

@ -0,0 +1,293 @@
package binary
import (
"fmt"
"math"
"strconv"
"go.mau.fi/whatsmeow/binary/token"
"go.mau.fi/whatsmeow/types"
)
type binaryEncoder struct {
data []byte
}
func newEncoder() *binaryEncoder {
return &binaryEncoder{[]byte{0}}
}
func (w *binaryEncoder) getData() []byte {
return w.data
}
func (w *binaryEncoder) pushByte(b byte) {
w.data = append(w.data, b)
}
func (w *binaryEncoder) pushBytes(bytes []byte) {
w.data = append(w.data, bytes...)
}
func (w *binaryEncoder) pushIntN(value, n int, littleEndian bool) {
for i := 0; i < n; i++ {
var curShift int
if littleEndian {
curShift = i
} else {
curShift = n - i - 1
}
w.pushByte(byte((value >> uint(curShift*8)) & 0xFF))
}
}
func (w *binaryEncoder) pushInt20(value int) {
w.pushBytes([]byte{byte((value >> 16) & 0x0F), byte((value >> 8) & 0xFF), byte(value & 0xFF)})
}
func (w *binaryEncoder) pushInt8(value int) {
w.pushIntN(value, 1, false)
}
func (w *binaryEncoder) pushInt16(value int) {
w.pushIntN(value, 2, false)
}
func (w *binaryEncoder) pushInt32(value int) {
w.pushIntN(value, 4, false)
}
func (w *binaryEncoder) pushString(value string) {
w.pushBytes([]byte(value))
}
func (w *binaryEncoder) writeByteLength(length int) {
if length < 256 {
w.pushByte(token.Binary8)
w.pushInt8(length)
} else if length < (1 << 20) {
w.pushByte(token.Binary20)
w.pushInt20(length)
} else if length < math.MaxInt32 {
w.pushByte(token.Binary32)
w.pushInt32(length)
} else {
panic(fmt.Errorf("length is too large: %d", length))
}
}
const tagSize = 1
func (w *binaryEncoder) writeNode(n Node) {
if n.Tag == "0" {
w.pushByte(token.List8)
w.pushByte(token.ListEmpty)
return
}
hasContent := 0
if n.Content != nil {
hasContent = 1
}
w.writeListStart(2*len(n.Attrs) + tagSize + hasContent)
w.writeString(n.Tag)
w.writeAttributes(n.Attrs)
if n.Content != nil {
w.write(n.Content)
}
}
func (w *binaryEncoder) write(data interface{}) {
switch typedData := data.(type) {
case nil:
w.pushByte(token.ListEmpty)
case types.JID:
w.writeJID(typedData)
case string:
w.writeString(typedData)
case int:
w.writeString(strconv.Itoa(typedData))
case int32:
w.writeString(strconv.FormatInt(int64(typedData), 10))
case uint:
w.writeString(strconv.FormatUint(uint64(typedData), 10))
case uint32:
w.writeString(strconv.FormatUint(uint64(typedData), 10))
case int64:
w.writeString(strconv.FormatInt(typedData, 10))
case uint64:
w.writeString(strconv.FormatUint(typedData, 10))
case bool:
w.writeString(strconv.FormatBool(typedData))
case []byte:
w.writeBytes(typedData)
case []Node:
w.writeListStart(len(typedData))
for _, n := range typedData {
w.writeNode(n)
}
default:
panic(fmt.Errorf("%w: %T", ErrInvalidType, typedData))
}
}
func (w *binaryEncoder) writeString(data string) {
var dictIndex byte
if tokenIndex, ok := token.IndexOfSingleToken(data); ok {
w.pushByte(tokenIndex)
} else if dictIndex, tokenIndex, ok = token.IndexOfDoubleByteToken(data); ok {
w.pushByte(token.Dictionary0 + dictIndex)
w.pushByte(tokenIndex)
} else if validateNibble(data) {
w.writePackedBytes(data, token.Nibble8)
} else if validateHex(data) {
w.writePackedBytes(data, token.Hex8)
} else {
w.writeStringRaw(data)
}
}
func (w *binaryEncoder) writeBytes(value []byte) {
w.writeByteLength(len(value))
w.pushBytes(value)
}
func (w *binaryEncoder) writeStringRaw(value string) {
w.writeByteLength(len(value))
w.pushString(value)
}
func (w *binaryEncoder) writeJID(jid types.JID) {
if jid.AD {
w.pushByte(token.ADJID)
w.pushByte(jid.Agent)
w.pushByte(jid.Device)
w.writeString(jid.User)
} else {
w.pushByte(token.JIDPair)
if len(jid.User) == 0 {
w.pushByte(token.ListEmpty)
} else {
w.write(jid.User)
}
w.write(jid.Server)
}
}
func (w *binaryEncoder) writeAttributes(attributes Attrs) {
if attributes == nil {
return
}
for key, val := range attributes {
if val == "" || val == nil {
continue
}
w.writeString(key)
w.write(val)
}
}
func (w *binaryEncoder) writeListStart(listSize int) {
if listSize == 0 {
w.pushByte(byte(token.ListEmpty))
} else if listSize < 256 {
w.pushByte(byte(token.List8))
w.pushInt8(listSize)
} else {
w.pushByte(byte(token.List16))
w.pushInt16(listSize)
}
}
func (w *binaryEncoder) writePackedBytes(value string, dataType int) {
if len(value) > token.PackedMax {
panic(fmt.Errorf("too many bytes to pack: %d", len(value)))
}
w.pushByte(byte(dataType))
roundedLength := byte(math.Ceil(float64(len(value)) / 2.0))
if len(value)%2 != 0 {
roundedLength |= 128
}
w.pushByte(roundedLength)
var packer func(byte) byte
if dataType == token.Nibble8 {
packer = packNibble
} else if dataType == token.Hex8 {
packer = packHex
} else {
// This should only be called with the correct values
panic(fmt.Errorf("invalid packed byte data type %v", dataType))
}
for i, l := 0, len(value)/2; i < l; i++ {
w.pushByte(w.packBytePair(packer, value[2*i], value[2*i+1]))
}
if len(value)%2 != 0 {
w.pushByte(w.packBytePair(packer, value[len(value)-1], '\x00'))
}
}
func (w *binaryEncoder) packBytePair(packer func(byte) byte, part1, part2 byte) byte {
return (packer(part1) << 4) | packer(part2)
}
func validateNibble(value string) bool {
if len(value) > token.PackedMax {
return false
}
for _, char := range value {
if !(char >= '0' && char <= '9') && char != '-' && char != '.' {
return false
}
}
return true
}
func packNibble(value byte) byte {
switch value {
case '-':
return 10
case '.':
return 11
case 0:
return 15
default:
if value >= '0' && value <= '9' {
return value - '0'
}
// This should be validated beforehand
panic(fmt.Errorf("invalid string to pack as nibble: %d / '%s'", value, string(value)))
}
}
func validateHex(value string) bool {
if len(value) > token.PackedMax {
return false
}
for _, char := range value {
if !(char >= '0' && char <= '9') && !(char >= 'A' && char <= 'F') && !(char >= 'a' && char <= 'f') {
return false
}
}
return true
}
func packHex(value byte) byte {
switch {
case value >= '0' && value <= '9':
return value - '0'
case value >= 'A' && value <= 'F':
return 10 + value - 'A'
case value >= 'a' && value <= 'f':
return 10 + value - 'a'
case value == 0:
return 15
default:
// This should be validated beforehand
panic(fmt.Errorf("invalid string to pack as hex: %d / '%s'", value, string(value)))
}
}

View File

@ -0,0 +1,12 @@
package binary
import "errors"
// Errors returned by the binary XML decoder.
var (
ErrInvalidType = errors.New("unsupported payload type")
ErrInvalidJIDType = errors.New("invalid JID type")
ErrInvalidNode = errors.New("invalid node")
ErrInvalidToken = errors.New("invalid token with tag")
ErrNonStringKey = errors.New("non-string key")
)

View File

@ -0,0 +1,83 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package binary implements encoding and decoding documents in WhatsApp's binary XML format.
package binary
// Attrs is a type alias for the attributes of an XML element (Node).
type Attrs = map[string]interface{}
// Node represents an XML element.
type Node struct {
Tag string // The tag of the element.
Attrs Attrs // The attributes of the element.
Content interface{} // The content inside the element. Can be nil, a list of Nodes, or a byte array.
}
// GetChildren returns the Content of the node as a list of nodes. If the content is not a list of nodes, this returns nil.
func (n *Node) GetChildren() []Node {
if n.Content == nil {
return nil
}
children, ok := n.Content.([]Node)
if !ok {
return nil
}
return children
}
// GetChildrenByTag returns the same list as GetChildren, but filters it by tag first.
func (n *Node) GetChildrenByTag(tag string) (children []Node) {
for _, node := range n.GetChildren() {
if node.Tag == tag {
children = append(children, node)
}
}
return
}
// GetOptionalChildByTag finds the first child with the given tag and returns it.
// Each provided tag will recurse in, so this is useful for getting a specific nested element.
func (n *Node) GetOptionalChildByTag(tags ...string) (val Node, ok bool) {
val = *n
Outer:
for _, tag := range tags {
for _, child := range val.GetChildren() {
if child.Tag == tag {
val = child
continue Outer
}
}
// If no matching children are found, return false
return
}
// All iterations of loop found a matching child, return it
ok = true
return
}
// GetChildByTag does the same thing as GetOptionalChildByTag, but returns the Node directly without the ok boolean.
func (n *Node) GetChildByTag(tags ...string) Node {
node, _ := n.GetOptionalChildByTag(tags...)
return node
}
// Marshal encodes an XML element (Node) into WhatsApp's binary XML representation.
func Marshal(n Node) ([]byte, error) {
w := newEncoder()
w.writeNode(n)
return w.getData(), nil
}
// Unmarshal decodes WhatsApp's binary XML representation into a Node.
func Unmarshal(data []byte) (*Node, error) {
r := newDecoder(data)
n, err := r.readNode()
if err != nil {
return nil, err
}
return n, nil
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
// Package proto contains the compiled protobuf structs from WhatsApp's protobuf schema.
package proto

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package binary
import (
"bytes"
"compress/zlib"
"fmt"
"io"
)
// Unpack unpacks the given decrypted data from the WhatsApp web API.
//
// It checks the first byte to decide whether to uncompress the data with zlib or just return as-is
// (without the first byte). There's currently no corresponding Pack function because Marshal
// already returns the data with a leading zero (i.e. not compressed).
func Unpack(data []byte) ([]byte, error) {
dataType, data := data[0], data[1:]
if 2&dataType > 0 {
if decompressor, err := zlib.NewReader(bytes.NewReader(data)); err != nil {
return nil, fmt.Errorf("failed to create zlib reader: %w", err)
} else if data, err = io.ReadAll(decompressor); err != nil {
return nil, err
}
}
return data, nil
}

108
vendor/go.mau.fi/whatsmeow/binary/xml.go vendored Normal file
View File

@ -0,0 +1,108 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package binary
import (
"encoding/hex"
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// Options to control how Node.XMLString behaves.
var (
IndentXML = false
MaxBytesToPrintAsHex = 128
)
// XMLString converts the Node to its XML representation
func (n *Node) XMLString() string {
content := n.contentString()
if len(content) == 0 {
return fmt.Sprintf("<%[1]s%[2]s/>", n.Tag, n.attributeString())
}
newline := "\n"
if len(content) == 1 || !IndentXML {
newline = ""
}
return fmt.Sprintf("<%[1]s%[2]s>%[4]s%[3]s%[4]s</%[1]s>", n.Tag, n.attributeString(), strings.Join(content, newline), newline)
}
func (n *Node) attributeString() string {
if len(n.Attrs) == 0 {
return ""
}
stringAttrs := make([]string, len(n.Attrs)+1)
i := 1
for key, value := range n.Attrs {
stringAttrs[i] = fmt.Sprintf(`%s="%v"`, key, value)
i++
}
sort.Strings(stringAttrs)
return strings.Join(stringAttrs, " ")
}
func printable(data []byte) string {
if !utf8.Valid(data) {
return ""
}
str := string(data)
for _, c := range str {
if !unicode.IsPrint(c) {
return ""
}
}
return str
}
func (n *Node) contentString() []string {
split := make([]string, 0)
switch content := n.Content.(type) {
case []Node:
for _, item := range content {
split = append(split, strings.Split(item.XMLString(), "\n")...)
}
case []byte:
if strContent := printable(content); len(strContent) > 0 {
if IndentXML {
split = append(split, strings.Split(string(content), "\n")...)
} else {
split = append(split, strings.ReplaceAll(string(content), "\n", "\\n"))
}
} else if len(content) > MaxBytesToPrintAsHex {
split = append(split, fmt.Sprintf("<!-- %d bytes -->", len(content)))
} else if !IndentXML {
split = append(split, hex.EncodeToString(content))
} else {
hexData := hex.EncodeToString(content)
for i := 0; i < len(hexData); i += 80 {
if len(hexData) < i+80 {
split = append(split, hexData[i:])
} else {
split = append(split, hexData[i:i+80])
}
}
}
case nil:
// don't append anything
default:
strContent := fmt.Sprintf("%s", content)
if IndentXML {
split = append(split, strings.Split(strContent, "\n")...)
} else {
split = append(split, strings.ReplaceAll(strContent, "\n", "\\n"))
}
}
if len(split) > 1 && IndentXML {
for i, line := range split {
split[i] = " " + line
}
}
return split
}

73
vendor/go.mau.fi/whatsmeow/call.go vendored Normal file
View File

@ -0,0 +1,73 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
func (cli *Client) handleCallEvent(node *waBinary.Node) {
go cli.sendAck(node)
if len(node.GetChildren()) != 1 {
cli.dispatchEvent(&events.UnknownCallEvent{Node: node})
return
}
ag := node.AttrGetter()
child := node.GetChildren()[0]
cag := child.AttrGetter()
basicMeta := types.BasicCallMeta{
From: ag.JID("from"),
Timestamp: time.Unix(ag.Int64("t"), 0),
CallCreator: cag.JID("call-creator"),
CallID: cag.String("call-id"),
}
switch child.Tag {
case "offer":
cli.dispatchEvent(&events.CallOffer{
BasicCallMeta: basicMeta,
CallRemoteMeta: types.CallRemoteMeta{
RemotePlatform: ag.String("platform"),
RemoteVersion: ag.String("version"),
},
Data: &child,
})
case "offer_notice":
cli.dispatchEvent(&events.CallOfferNotice{
BasicCallMeta: basicMeta,
Media: cag.String("media"),
Type: cag.String("type"),
Data: &child,
})
case "relaylatency":
cli.dispatchEvent(&events.CallRelayLatency{
BasicCallMeta: basicMeta,
Data: &child,
})
case "accept":
cli.dispatchEvent(&events.CallAccept{
BasicCallMeta: basicMeta,
CallRemoteMeta: types.CallRemoteMeta{
RemotePlatform: ag.String("platform"),
RemoteVersion: ag.String("version"),
},
Data: &child,
})
case "terminate":
cli.dispatchEvent(&events.CallTerminate{
BasicCallMeta: basicMeta,
Reason: cag.String("reason"),
Data: &child,
})
default:
cli.dispatchEvent(&events.UnknownCallEvent{Node: node})
}
}

459
vendor/go.mau.fi/whatsmeow/client.go vendored Normal file
View File

@ -0,0 +1,459 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package whatsmeow implements a client for interacting with the WhatsApp web multidevice API.
package whatsmeow
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/socket"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
)
// EventHandler is a function that can handle events from WhatsApp.
type EventHandler func(evt interface{})
type nodeHandler func(node *waBinary.Node)
var nextHandlerID uint32
type wrappedEventHandler struct {
fn EventHandler
id uint32
}
// Client contains everything necessary to connect to and interact with the WhatsApp web API.
type Client struct {
Store *store.Device
Log waLog.Logger
recvLog waLog.Logger
sendLog waLog.Logger
socket *socket.NoiseSocket
socketLock sync.RWMutex
isLoggedIn uint32
expectedDisconnectVal uint32
EnableAutoReconnect bool
LastSuccessfulConnect time.Time
AutoReconnectErrors int
// EmitAppStateEventsOnFullSync can be set to true if you want to get app state events emitted
// even when re-syncing the whole state.
EmitAppStateEventsOnFullSync bool
appStateProc *appstate.Processor
appStateSyncLock sync.Mutex
uploadPreKeysLock sync.Mutex
lastPreKeyUpload time.Time
mediaConn *MediaConn
mediaConnLock sync.Mutex
responseWaiters map[string]chan<- *waBinary.Node
responseWaitersLock sync.Mutex
nodeHandlers map[string]nodeHandler
handlerQueue chan *waBinary.Node
eventHandlers []wrappedEventHandler
eventHandlersLock sync.RWMutex
messageRetries map[string]int
messageRetriesLock sync.Mutex
privacySettingsCache atomic.Value
groupParticipantsCache map[types.JID][]types.JID
groupParticipantsCacheLock sync.Mutex
userDevicesCache map[types.JID][]types.JID
userDevicesCacheLock sync.Mutex
recentMessagesMap map[recentMessageKey]*waProto.Message
recentMessagesList [recentMessagesSize]recentMessageKey
recentMessagesPtr int
recentMessagesLock sync.RWMutex
// GetMessageForRetry is used to find the source message for handling retry receipts
// when the message is not found in the recently sent message cache.
GetMessageForRetry func(to types.JID, id types.MessageID) *waProto.Message
// PreRetryCallback is called before a retry receipt is accepted.
// If it returns false, the accepting will be cancelled and the retry receipt will be ignored.
PreRetryCallback func(receipt *events.Receipt, retryCount int, msg *waProto.Message) bool
uniqueID string
idCounter uint32
}
// Size of buffer for the channel that all incoming XML nodes go through.
// In general it shouldn't go past a few buffered messages, but the channel is big to be safe.
const handlerQueueSize = 2048
// NewClient initializes a new WhatsApp web client.
//
// The logger can be nil, it will default to a no-op logger.
//
// The device store must be set. A default SQL-backed implementation is available in the store/sqlstore package.
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
// if err != nil {
// panic(err)
// }
// // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
// deviceStore, err := container.GetFirstDevice()
// if err != nil {
// panic(err)
// }
// client := whatsmeow.NewClient(deviceStore, nil)
func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
if log == nil {
log = waLog.Noop
}
randomBytes := make([]byte, 2)
_, _ = rand.Read(randomBytes)
cli := &Client{
Store: deviceStore,
Log: log,
recvLog: log.Sub("Recv"),
sendLog: log.Sub("Send"),
uniqueID: fmt.Sprintf("%d.%d-", randomBytes[0], randomBytes[1]),
responseWaiters: make(map[string]chan<- *waBinary.Node),
eventHandlers: make([]wrappedEventHandler, 0, 1),
messageRetries: make(map[string]int),
handlerQueue: make(chan *waBinary.Node, handlerQueueSize),
appStateProc: appstate.NewProcessor(deviceStore, log.Sub("AppState")),
groupParticipantsCache: make(map[types.JID][]types.JID),
userDevicesCache: make(map[types.JID][]types.JID),
recentMessagesMap: make(map[recentMessageKey]*waProto.Message, recentMessagesSize),
GetMessageForRetry: func(to types.JID, id types.MessageID) *waProto.Message { return nil },
EnableAutoReconnect: true,
}
cli.nodeHandlers = map[string]nodeHandler{
"message": cli.handleEncryptedMessage,
"receipt": cli.handleReceipt,
"call": cli.handleCallEvent,
"chatstate": cli.handleChatState,
"presence": cli.handlePresence,
"notification": cli.handleNotification,
"success": cli.handleConnectSuccess,
"failure": cli.handleConnectFailure,
"stream:error": cli.handleStreamError,
"iq": cli.handleIQ,
"ib": cli.handleIB,
}
return cli
}
// Connect connects the client to the WhatsApp web websocket. After connection, it will either
// authenticate if there's data in the device store, or emit a QREvent to set up a new link.
func (cli *Client) Connect() error {
cli.socketLock.Lock()
defer cli.socketLock.Unlock()
if cli.socket != nil {
if !cli.socket.IsConnected() {
cli.unlockedDisconnect()
} else {
return ErrAlreadyConnected
}
}
cli.resetExpectedDisconnect()
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), socket.WAConnHeader)
if err := fs.Connect(); err != nil {
fs.Close(0)
return err
} else if err = cli.doHandshake(fs, *keys.NewKeyPair()); err != nil {
fs.Close(0)
return fmt.Errorf("noise handshake failed: %w", err)
}
go cli.keepAliveLoop(cli.socket.Context())
go cli.handlerQueueLoop(cli.socket.Context())
return nil
}
// IsLoggedIn returns true after the client is successfully connected and authenticated on WhatsApp.
func (cli *Client) IsLoggedIn() bool {
return atomic.LoadUint32(&cli.isLoggedIn) == 1
}
func (cli *Client) onDisconnect(ns *socket.NoiseSocket, remote bool) {
ns.Stop(false)
cli.socketLock.Lock()
defer cli.socketLock.Unlock()
if cli.socket == ns {
cli.socket = nil
cli.clearResponseWaiters()
if !cli.isExpectedDisconnect() && remote {
cli.Log.Debugf("Emitting Disconnected event")
go cli.dispatchEvent(&events.Disconnected{})
go cli.autoReconnect()
} else if remote {
cli.Log.Debugf("OnDisconnect() called, but it was expected, so not emitting event")
} else {
cli.Log.Debugf("OnDisconnect() called after manual disconnection")
}
} else {
cli.Log.Debugf("Ignoring OnDisconnect on different socket")
}
}
func (cli *Client) expectDisconnect() {
atomic.StoreUint32(&cli.expectedDisconnectVal, 1)
}
func (cli *Client) resetExpectedDisconnect() {
atomic.StoreUint32(&cli.expectedDisconnectVal, 0)
}
func (cli *Client) isExpectedDisconnect() bool {
return atomic.LoadUint32(&cli.expectedDisconnectVal) == 1
}
func (cli *Client) autoReconnect() {
if !cli.EnableAutoReconnect || cli.Store.ID == nil {
return
}
for {
cli.AutoReconnectErrors++
autoReconnectDelay := time.Duration(cli.AutoReconnectErrors) * 2 * time.Second
cli.Log.Debugf("Automatically reconnecting after %v", autoReconnectDelay)
time.Sleep(autoReconnectDelay)
err := cli.Connect()
if errors.Is(err, ErrAlreadyConnected) {
cli.Log.Debugf("Connect() said we're already connected after autoreconnect sleep")
return
} else if err != nil {
cli.Log.Errorf("Error reconnecting after autoreconnect sleep: %v", err)
} else {
return
}
}
}
// IsConnected checks if the client is connected to the WhatsApp web websocket.
// Note that this doesn't check if the client is authenticated. See the IsLoggedIn field for that.
func (cli *Client) IsConnected() bool {
cli.socketLock.RLock()
connected := cli.socket != nil && cli.socket.IsConnected()
cli.socketLock.RUnlock()
return connected
}
// Disconnect disconnects from the WhatsApp web websocket.
func (cli *Client) Disconnect() {
if cli.socket == nil {
return
}
cli.socketLock.Lock()
cli.unlockedDisconnect()
cli.socketLock.Unlock()
}
// Disconnect closes the websocket connection.
func (cli *Client) unlockedDisconnect() {
if cli.socket != nil {
cli.socket.Stop(true)
cli.socket = nil
}
}
// Logout sends a request to unlink the device, then disconnects from the websocket and deletes the local device store.
//
// If the logout request fails, the disconnection and local data deletion will not happen either.
// If an error is returned, but you want to force disconnect/clear data, call Client.Disconnect() and Client.Store.Delete() manually.
func (cli *Client) Logout() error {
if cli.Store.ID == nil {
return ErrNotLoggedIn
}
_, err := cli.sendIQ(infoQuery{
Namespace: "md",
Type: "set",
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "remove-companion-device",
Attrs: waBinary.Attrs{
"jid": *cli.Store.ID,
"reason": "user_initiated",
},
}},
})
if err != nil {
return fmt.Errorf("error sending logout request: %w", err)
}
cli.Disconnect()
err = cli.Store.Delete()
if err != nil {
return fmt.Errorf("error deleting data from store: %w", err)
}
return nil
}
// AddEventHandler registers a new function to receive all events emitted by this client.
//
// The returned integer is the event handler ID, which can be passed to RemoveEventHandler to remove it.
//
// All registered event handlers will receive all events. You should use a type switch statement to
// filter the events you want:
// func myEventHandler(evt interface{}) {
// switch v := evt.(type) {
// case *events.Message:
// fmt.Println("Received a message!")
// case *events.Receipt:
// fmt.Println("Received a receipt!")
// }
// }
//
// If you want to access the Client instance inside the event handler, the recommended way is to
// wrap the whole handler in another struct:
// type MyClient struct {
// WAClient *whatsmeow.Client
// eventHandlerID uint32
// }
//
// func (mycli *MyClient) register() {
// mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler)
// }
//
// func (mycli *MyClient) myEventHandler(evt interface{}) {
// // Handle event and access mycli.WAClient
// }
func (cli *Client) AddEventHandler(handler EventHandler) uint32 {
nextID := atomic.AddUint32(&nextHandlerID, 1)
cli.eventHandlersLock.Lock()
cli.eventHandlers = append(cli.eventHandlers, wrappedEventHandler{handler, nextID})
cli.eventHandlersLock.Unlock()
return nextID
}
// RemoveEventHandler removes a previously registered event handler function.
// If the function with the given ID is found, this returns true.
//
// N.B. Do not run this directly from an event handler. That would cause a deadlock because the
// event dispatcher holds a read lock on the event handler list, and this method wants a write lock
// on the same list. Instead run it in a goroutine:
// func (mycli *MyClient) myEventHandler(evt interface{}) {
// if noLongerWantEvents {
// go mycli.WAClient.RemoveEventHandler(mycli.eventHandlerID)
// }
// }
func (cli *Client) RemoveEventHandler(id uint32) bool {
cli.eventHandlersLock.Lock()
defer cli.eventHandlersLock.Unlock()
for index := range cli.eventHandlers {
if cli.eventHandlers[index].id == id {
if index == 0 {
cli.eventHandlers[0].fn = nil
cli.eventHandlers = cli.eventHandlers[1:]
return true
} else if index < len(cli.eventHandlers)-1 {
copy(cli.eventHandlers[index:], cli.eventHandlers[index+1:])
}
cli.eventHandlers[len(cli.eventHandlers)-1].fn = nil
cli.eventHandlers = cli.eventHandlers[:len(cli.eventHandlers)-1]
return true
}
}
return false
}
// RemoveEventHandlers removes all event handlers that have been registered with AddEventHandler
func (cli *Client) RemoveEventHandlers() {
cli.eventHandlersLock.Lock()
cli.eventHandlers = make([]wrappedEventHandler, 0, 1)
cli.eventHandlersLock.Unlock()
}
func (cli *Client) handleFrame(data []byte) {
decompressed, err := waBinary.Unpack(data)
if err != nil {
cli.Log.Warnf("Failed to decompress frame: %v", err)
cli.Log.Debugf("Errored frame hex: %s", hex.EncodeToString(data))
return
}
node, err := waBinary.Unmarshal(decompressed)
if err != nil {
cli.Log.Warnf("Failed to decode node in frame: %v", err)
cli.Log.Debugf("Errored frame hex: %s", hex.EncodeToString(decompressed))
return
}
cli.recvLog.Debugf("%s", node.XMLString())
if node.Tag == "xmlstreamend" {
if !cli.isExpectedDisconnect() {
cli.Log.Warnf("Received stream end frame")
}
// TODO should we do something else?
} else if cli.receiveResponse(node) {
// handled
} else if _, ok := cli.nodeHandlers[node.Tag]; ok {
select {
case cli.handlerQueue <- node:
default:
cli.Log.Warnf("Handler queue is full, message ordering is no longer guaranteed")
go func() {
cli.handlerQueue <- node
}()
}
} else {
cli.Log.Debugf("Didn't handle WhatsApp node %s", node.Tag)
}
}
func (cli *Client) handlerQueueLoop(ctx context.Context) {
for {
select {
case node := <-cli.handlerQueue:
cli.nodeHandlers[node.Tag](node)
case <-ctx.Done():
return
}
}
}
func (cli *Client) sendNode(node waBinary.Node) error {
cli.socketLock.RLock()
sock := cli.socket
cli.socketLock.RUnlock()
if sock == nil {
return ErrNotConnected
}
payload, err := waBinary.Marshal(node)
if err != nil {
return fmt.Errorf("failed to marshal node: %w", err)
}
cli.sendLog.Debugf("%s", node.XMLString())
return sock.SendFrame(payload)
}
func (cli *Client) dispatchEvent(evt interface{}) {
cli.eventHandlersLock.RLock()
defer func() {
cli.eventHandlersLock.RUnlock()
err := recover()
if err != nil {
cli.Log.Errorf("Event handler panicked while handling a %T: %v\n%s", evt, err, debug.Stack())
}
}()
for _, handler := range cli.eventHandlers {
handler.fn(evt)
}
}

View File

@ -0,0 +1,141 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"sync/atomic"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
func (cli *Client) handleStreamError(node *waBinary.Node) {
atomic.StoreUint32(&cli.isLoggedIn, 0)
code, _ := node.Attrs["code"].(string)
conflict, _ := node.GetOptionalChildByTag("conflict")
conflictType := conflict.AttrGetter().OptionalString("type")
switch {
case code == "515":
cli.Log.Infof("Got 515 code, reconnecting...")
go func() {
cli.Disconnect()
err := cli.Connect()
if err != nil {
cli.Log.Errorf("Failed to reconnect after 515 code:", err)
}
}()
case code == "401" && conflictType == "device_removed":
cli.expectDisconnect()
cli.Log.Infof("Got device removed stream error, sending LoggedOut event and deleting session")
go cli.dispatchEvent(&events.LoggedOut{OnConnect: false})
err := cli.Store.Delete()
if err != nil {
cli.Log.Warnf("Failed to delete store after device_removed error: %v", err)
}
case conflictType == "replaced":
cli.expectDisconnect()
cli.Log.Infof("Got replaced stream error, sending StreamReplaced event")
go cli.dispatchEvent(&events.StreamReplaced{})
case code == "503":
// This seems to happen when the server wants to restart or something.
// The disconnection will be emitted as an events.Disconnected and then the auto-reconnect will do its thing.
cli.Log.Warnf("Got 503 stream error, assuming automatic reconnect will handle it")
default:
cli.Log.Errorf("Unknown stream error: %s", node.XMLString())
go cli.dispatchEvent(&events.StreamError{Code: code, Raw: node})
}
}
func (cli *Client) handleIB(node *waBinary.Node) {
children := node.GetChildren()
for _, child := range children {
ag := child.AttrGetter()
switch child.Tag {
case "downgrade_webclient":
go cli.dispatchEvent(&events.QRScannedWithoutMultidevice{})
case "offline_preview":
cli.dispatchEvent(&events.OfflineSyncPreview{
Total: ag.Int("count"),
AppDataChanges: ag.Int("appdata"),
Messages: ag.Int("message"),
Notifications: ag.Int("notification"),
Receipts: ag.Int("receipt"),
})
case "offline":
cli.dispatchEvent(&events.OfflineSyncCompleted{
Count: ag.Int("count"),
})
}
}
}
func (cli *Client) handleConnectFailure(node *waBinary.Node) {
ag := node.AttrGetter()
reason := ag.String("reason")
if reason == "401" {
cli.expectDisconnect()
cli.Log.Infof("Got 401 connect failure, sending LoggedOut event and deleting session")
go cli.dispatchEvent(&events.LoggedOut{OnConnect: true})
err := cli.Store.Delete()
if err != nil {
cli.Log.Warnf("Failed to delete store after 401 failure: %v", err)
}
} else {
cli.expectDisconnect()
cli.Log.Warnf("Unknown connect failure: %s", node.XMLString())
go cli.dispatchEvent(&events.ConnectFailure{Reason: reason, Raw: node})
}
}
func (cli *Client) handleConnectSuccess(node *waBinary.Node) {
cli.Log.Infof("Successfully authenticated")
cli.LastSuccessfulConnect = time.Now()
cli.AutoReconnectErrors = 0
atomic.StoreUint32(&cli.isLoggedIn, 1)
go func() {
if dbCount, err := cli.Store.PreKeys.UploadedPreKeyCount(); err != nil {
cli.Log.Errorf("Failed to get number of prekeys in database: %v", err)
} else if serverCount, err := cli.getServerPreKeyCount(); err != nil {
cli.Log.Warnf("Failed to get number of prekeys on server: %v", err)
} else {
cli.Log.Debugf("Database has %d prekeys, server says we have %d", dbCount, serverCount)
if serverCount < MinPreKeyCount || dbCount < MinPreKeyCount {
cli.uploadPreKeys()
sc, _ := cli.getServerPreKeyCount()
cli.Log.Debugf("Prekey count after upload: %d", sc)
}
}
err := cli.SetPassive(false)
if err != nil {
cli.Log.Warnf("Failed to send post-connect passive IQ: %v", err)
}
cli.dispatchEvent(&events.Connected{})
}()
}
// SetPassive tells the WhatsApp server whether this device is passive or not.
//
// This seems to mostly affect whether the device receives certain events.
// By default, whatsmeow will automatically do SetPassive(false) after connecting.
func (cli *Client) SetPassive(passive bool) error {
tag := "active"
if passive {
tag = "passive"
}
_, err := cli.sendIQ(infoQuery{
Namespace: "passive",
Type: "set",
To: types.ServerJID,
Content: []waBinary.Node{{Tag: tag}},
})
if err != nil {
return err
}
return nil
}

213
vendor/go.mau.fi/whatsmeow/download.go vendored Normal file
View File

@ -0,0 +1,213 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/util/cbcutil"
"go.mau.fi/whatsmeow/util/hkdfutil"
)
// MediaType represents a type of uploaded file on WhatsApp.
// The value is the key which is used as a part of generating the encryption keys.
type MediaType string
// The known media types
const (
MediaImage MediaType = "WhatsApp Image Keys"
MediaVideo MediaType = "WhatsApp Video Keys"
MediaAudio MediaType = "WhatsApp Audio Keys"
MediaDocument MediaType = "WhatsApp Document Keys"
MediaHistory MediaType = "WhatsApp History Keys"
MediaAppState MediaType = "WhatsApp App State Keys"
)
// DownloadableMessage represents a protobuf message that contains attachment info.
type DownloadableMessage interface {
proto.Message
GetDirectPath() string
GetMediaKey() []byte
GetFileSha256() []byte
GetFileEncSha256() []byte
}
// All the message types that are intended to be downloadable
var (
_ DownloadableMessage = (*waProto.ImageMessage)(nil)
_ DownloadableMessage = (*waProto.AudioMessage)(nil)
_ DownloadableMessage = (*waProto.VideoMessage)(nil)
_ DownloadableMessage = (*waProto.DocumentMessage)(nil)
_ DownloadableMessage = (*waProto.StickerMessage)(nil)
_ DownloadableMessage = (*waProto.HistorySyncNotification)(nil)
_ DownloadableMessage = (*waProto.ExternalBlobReference)(nil)
)
type downloadableMessageWithLength interface {
DownloadableMessage
GetFileLength() uint64
}
type downloadableMessageWithSizeBytes interface {
DownloadableMessage
GetFileSizeBytes() uint64
}
type downloadableMessageWithURL interface {
DownloadableMessage
GetUrl() string
}
var classToMediaType = map[protoreflect.Name]MediaType{
"ImageMessage": MediaImage,
"AudioMessage": MediaAudio,
"VideoMessage": MediaVideo,
"DocumentMessage": MediaDocument,
"StickerMessage": MediaImage,
"HistorySyncNotification": MediaHistory,
"ExternalBlobReference": MediaAppState,
}
var mediaTypeToMMSType = map[MediaType]string{
MediaImage: "image",
MediaAudio: "audio",
MediaVideo: "video",
MediaDocument: "document",
MediaHistory: "md-msg-hist",
MediaAppState: "md-app-state",
}
// DownloadAny loops through the downloadable parts of the given message and downloads the first non-nil item.
func (cli *Client) DownloadAny(msg *waProto.Message) (data []byte, err error) {
downloadables := []DownloadableMessage{msg.GetImageMessage(), msg.GetAudioMessage(), msg.GetVideoMessage(), msg.GetDocumentMessage(), msg.GetStickerMessage()}
for _, downloadable := range downloadables {
if downloadable != nil {
return cli.Download(downloadable)
}
}
return nil, ErrNothingDownloadableFound
}
func getSize(msg DownloadableMessage) int {
switch sized := msg.(type) {
case downloadableMessageWithLength:
return int(sized.GetFileLength())
case downloadableMessageWithSizeBytes:
return int(sized.GetFileSizeBytes())
default:
return -1
}
}
// Download downloads the attachment from the given protobuf message.
func (cli *Client) Download(msg DownloadableMessage) (data []byte, err error) {
mediaType, ok := classToMediaType[msg.ProtoReflect().Descriptor().Name()]
if !ok {
return nil, fmt.Errorf("%w '%s'", ErrUnknownMediaType, string(msg.ProtoReflect().Descriptor().Name()))
}
urlable, ok := msg.(downloadableMessageWithURL)
if ok && len(urlable.GetUrl()) > 0 {
return downloadAndDecrypt(urlable.GetUrl(), msg.GetMediaKey(), mediaType, getSize(msg), msg.GetFileEncSha256(), msg.GetFileSha256())
} else if len(msg.GetDirectPath()) > 0 {
return cli.downloadMediaWithPath(msg.GetDirectPath(), msg.GetFileEncSha256(), msg.GetFileSha256(), msg.GetMediaKey(), getSize(msg), mediaType, mediaTypeToMMSType[mediaType])
} else {
return nil, ErrNoURLPresent
}
}
func (cli *Client) downloadMediaWithPath(directPath string, encFileHash, fileHash, mediaKey []byte, fileLength int, mediaType MediaType, mmsType string) (data []byte, err error) {
err = cli.refreshMediaConn(false)
if err != nil {
return nil, fmt.Errorf("failed to refresh media connections: %w", err)
}
for i, host := range cli.mediaConn.Hosts {
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
data, err = downloadAndDecrypt(mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash)
// TODO there are probably some errors that shouldn't retry
if err != nil {
if i >= len(cli.mediaConn.Hosts)-1 {
return nil, fmt.Errorf("failed to download media from last host: %w", err)
}
cli.Log.Warnf("Failed to download media: %s, trying with next host...", err)
}
}
return
}
func downloadAndDecrypt(url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSha256, fileSha256 []byte) (data []byte, err error) {
iv, cipherKey, macKey, _ := getMediaKeys(mediaKey, appInfo)
var ciphertext, mac []byte
if ciphertext, mac, err = downloadEncryptedMedia(url, fileEncSha256); err != nil {
} else if err = validateMedia(iv, ciphertext, macKey, mac); err != nil {
} else if data, err = cbcutil.Decrypt(cipherKey, iv, ciphertext); err != nil {
err = fmt.Errorf("failed to decrypt file: %w", err)
} else if fileLength >= 0 && len(data) != fileLength {
err = fmt.Errorf("%w: expected %d, got %d", ErrFileLengthMismatch, fileLength, len(data))
} else if len(fileSha256) == 32 && sha256.Sum256(data) != *(*[32]byte)(fileSha256) {
err = ErrInvalidMediaSHA256
}
return
}
func getMediaKeys(mediaKey []byte, appInfo MediaType) (iv, cipherKey, macKey, refKey []byte) {
mediaKeyExpanded := hkdfutil.SHA256(mediaKey, nil, []byte(appInfo), 112)
return mediaKeyExpanded[:16], mediaKeyExpanded[16:48], mediaKeyExpanded[48:80], mediaKeyExpanded[80:]
}
func downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
var resp *http.Response
resp, err = http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
err = ErrMediaDownloadFailedWith404
} else if resp.StatusCode == http.StatusGone {
err = ErrMediaDownloadFailedWith410
} else {
err = fmt.Errorf("download failed with status code %d", resp.StatusCode)
}
return
}
var data []byte
data, err = io.ReadAll(resp.Body)
if err != nil {
return
} else if len(data) <= 10 {
err = ErrTooShortFile
return
}
file, mac = data[:len(data)-10], data[len(data)-10:]
if len(checksum) == 32 && sha256.Sum256(data) != *(*[32]byte)(checksum) {
err = ErrInvalidMediaEncSHA256
}
return
}
func validateMedia(iv, file, macKey, mac []byte) error {
h := hmac.New(sha256.New, macKey)
h.Write(iv)
h.Write(file)
if !hmac.Equal(h.Sum(nil)[:10], mac) {
return ErrInvalidMediaHMAC
}
return nil
}

157
vendor/go.mau.fi/whatsmeow/errors.go vendored Normal file
View File

@ -0,0 +1,157 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"errors"
"fmt"
waBinary "go.mau.fi/whatsmeow/binary"
)
// Miscellaneous errors
var (
ErrNoSession = errors.New("can't encrypt message for device: no signal session established")
ErrIQTimedOut = errors.New("info query timed out")
ErrIQDisconnected = errors.New("websocket disconnected before info query returned response")
ErrNotConnected = errors.New("websocket not connected")
ErrNotLoggedIn = errors.New("the store doesn't contain a device JID")
ErrAlreadyConnected = errors.New("websocket is already connected")
ErrQRAlreadyConnected = errors.New("GetQRChannel must be called before connecting")
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")
)
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).
ErrProfilePictureUnauthorized = errors.New("the user has hidden their profile picture from you")
// ErrGroupInviteLinkUnauthorized is returned by GetGroupInviteLink if you don't have the permission to get the link (status code 401).
ErrGroupInviteLinkUnauthorized = errors.New("you don't have the permission to get the group's invite link")
// ErrNotInGroup is returned by group info getting methods if you're not in the group (status code 403).
ErrNotInGroup = errors.New("you're not participating in that group")
// ErrGroupNotFound is returned by group info getting methods if the group doesn't exist (status code 404).
ErrGroupNotFound = errors.New("that group does not exist")
// ErrInviteLinkInvalid is returned by methods that use group invite links if the invite link is malformed.
ErrInviteLinkInvalid = errors.New("that group invite link is not valid")
// ErrInviteLinkRevoked is returned by methods that use group invite links if the invite link was valid, but has been revoked and can no longer be used.
ErrInviteLinkRevoked = errors.New("that group invite link has been revoked")
// ErrBusinessMessageLinkNotFound is returned by ResolveBusinessMessageLink if the link doesn't exist or has been revoked.
ErrBusinessMessageLinkNotFound = errors.New("that business message link does not exist or has been revoked")
)
// Some errors that Client.SendMessage can return
var (
ErrBroadcastListUnsupported = errors.New("sending to 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")
ErrSendDisconnected = errors.New("websocket disconnected before message send returned response")
)
// Some errors that Client.Download can return
var (
ErrMediaDownloadFailedWith404 = errors.New("download failed with status code 404")
ErrMediaDownloadFailedWith410 = errors.New("download failed with status code 410")
ErrNoURLPresent = errors.New("no url present")
ErrFileLengthMismatch = errors.New("file length does not match")
ErrTooShortFile = errors.New("file too short")
ErrInvalidMediaHMAC = errors.New("invalid media hmac")
ErrInvalidMediaEncSHA256 = errors.New("hash of media ciphertext doesn't match")
ErrInvalidMediaSHA256 = errors.New("hash of media plaintext doesn't match")
ErrUnknownMediaType = errors.New("unknown media type")
ErrNothingDownloadableFound = errors.New("didn't find any attachments in message")
)
type wrappedIQError struct {
HumanError error
IQError error
}
func (err *wrappedIQError) Error() string {
return err.HumanError.Error()
}
func (err *wrappedIQError) Is(other error) bool {
return errors.Is(other, err.HumanError)
}
func (err *wrappedIQError) Unwrap() error {
return err.IQError
}
func wrapIQError(human, iq error) error {
return &wrappedIQError{human, iq}
}
// IQError is a generic error container for info queries
type IQError struct {
Code int
Text string
ErrorNode *waBinary.Node
RawNode *waBinary.Node
}
// Common errors returned by info queries for use with errors.Is
var (
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"}
)
func parseIQError(node *waBinary.Node) error {
var err IQError
err.RawNode = node
val, ok := node.GetOptionalChildByTag("error")
if ok {
err.ErrorNode = &val
ag := val.AttrGetter()
err.Code = ag.OptionalInt("code")
err.Text = ag.OptionalString("text")
}
return &err
}
func (iqe *IQError) Error() string {
if iqe.Code == 0 {
if iqe.ErrorNode != nil {
return fmt.Sprintf("info query returned unknown error: %s", iqe.ErrorNode.XMLString())
} else if iqe.RawNode != nil {
return fmt.Sprintf("info query returned unexpected response: %s", iqe.RawNode.XMLString())
} else {
return "unknown info query error"
}
}
return fmt.Sprintf("info query returned status %d: %s", iqe.Code, iqe.Text)
}
func (iqe *IQError) Is(other error) bool {
otherIQE, ok := other.(*IQError)
if !ok {
return false
} else if iqe.Code != 0 && otherIQE.Code != 0 {
return otherIQE.Code == iqe.Code && otherIQE.Text == iqe.Text
} else if iqe.ErrorNode != nil && otherIQE.ErrorNode != nil {
return iqe.ErrorNode.XMLString() == otherIQE.ErrorNode.XMLString()
} else {
return false
}
}
// ElementMissingError is returned by various functions that parse XML elements when a required element is missing.
type ElementMissingError struct {
Tag string
In string
}
func (eme *ElementMissingError) Error() string {
return fmt.Sprintf("missing <%s> element in %s", eme.Tag, eme.In)
}

566
vendor/go.mau.fi/whatsmeow/group.go vendored Normal file
View File

@ -0,0 +1,566 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"errors"
"fmt"
"strings"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
const InviteLinkPrefix = "https://chat.whatsapp.com/"
func (cli *Client) sendGroupIQ(iqType infoQueryType, jid types.JID, content waBinary.Node) (*waBinary.Node, error) {
return cli.sendIQ(infoQuery{
Namespace: "w:g2",
Type: iqType,
To: jid,
Content: []waBinary.Node{content},
})
}
// 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.
func (cli *Client) CreateGroup(name string, participants []types.JID) (*types.GroupInfo, error) {
participantNodes := make([]waBinary.Node, len(participants))
for i, participant := range participants {
participantNodes[i] = waBinary.Node{
Tag: "participant",
Attrs: waBinary.Attrs{"jid": participant},
}
}
key := GenerateMessageID()
resp, err := cli.sendGroupIQ(iqSet, types.GroupServerJID, waBinary.Node{
Tag: "create",
Attrs: waBinary.Attrs{
"subject": name,
"key": key,
},
Content: participantNodes,
})
if err != nil {
return nil, err
}
groupNode, ok := resp.GetOptionalChildByTag("group")
if !ok {
return nil, &ElementMissingError{Tag: "group", In: "response to create group query"}
}
return cli.parseGroupNode(&groupNode)
}
// LeaveGroup leaves the specified group on WhatsApp.
func (cli *Client) LeaveGroup(jid types.JID) error {
_, err := cli.sendGroupIQ(iqSet, types.GroupServerJID, waBinary.Node{
Tag: "leave",
Content: []waBinary.Node{{
Tag: "group",
Attrs: waBinary.Attrs{"id": jid},
}},
})
return err
}
type ParticipantChange string
const (
ParticipantChangeAdd ParticipantChange = "add"
ParticipantChangeRemove ParticipantChange = "remove"
ParticipantChangePromote ParticipantChange = "promote"
ParticipantChangeDemote ParticipantChange = "demote"
)
// UpdateGroupParticipants can be used to add, remove, promote and demote members in a WhatsApp group.
func (cli *Client) UpdateGroupParticipants(jid types.JID, participantChanges map[types.JID]ParticipantChange) (*waBinary.Node, error) {
content := make([]waBinary.Node, len(participantChanges))
i := 0
for participantJID, change := range participantChanges {
content[i] = waBinary.Node{
Tag: string(change),
Content: []waBinary.Node{{
Tag: "participant",
Attrs: waBinary.Attrs{"jid": participantJID},
}},
}
i++
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:g2",
Type: iqSet,
To: jid,
Content: content,
})
if err != nil {
return nil, err
}
// TODO proper return value?
return resp, nil
}
// SetGroupName updates the name (subject) of the given group on WhatsApp.
func (cli *Client) SetGroupName(jid types.JID, name string) error {
_, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{
Tag: "subject",
Content: []byte(name),
})
return err
}
// SetGroupTopic updates the topic (description) of the given group on WhatsApp.
//
// The previousID and newID fields are optional. If the previous ID is not specified, this will
// automatically fetch the current group info to find the previous topic ID. If the new ID is not
// specified, one will be generated with GenerateMessageID().
func (cli *Client) SetGroupTopic(jid types.JID, previousID, newID, topic string) error {
if previousID == "" {
oldInfo, err := cli.GetGroupInfo(jid)
if err != nil {
return fmt.Errorf("failed to get old group info to update topic: %v", err)
}
previousID = oldInfo.TopicID
}
if newID == "" {
newID = GenerateMessageID()
}
_, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{
Tag: "description",
Attrs: waBinary.Attrs{
"prev": previousID,
"id": newID,
},
Content: []waBinary.Node{{
Tag: "body",
Content: []byte(topic),
}},
})
return err
}
// SetGroupLocked changes whether the group is locked (i.e. whether only admins can modify group info).
func (cli *Client) SetGroupLocked(jid types.JID, locked bool) error {
tag := "locked"
if !locked {
tag = "unlocked"
}
_, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{Tag: tag})
return err
}
// SetGroupAnnounce changes whether the group is in announce mode (i.e. whether only admins can send messages).
func (cli *Client) SetGroupAnnounce(jid types.JID, announce bool) error {
tag := "announcement"
if !announce {
tag = "not_announcement"
}
_, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{Tag: tag})
return err
}
// GetGroupInviteLink requests the invite link to the group from the WhatsApp servers.
//
// If reset is true, then the old invite link will be revoked and a new one generated.
func (cli *Client) GetGroupInviteLink(jid types.JID, reset bool) (string, error) {
iqType := iqGet
if reset {
iqType = iqSet
}
resp, err := cli.sendGroupIQ(iqType, jid, waBinary.Node{Tag: "invite"})
if errors.Is(err, ErrIQNotAuthorized) {
return "", wrapIQError(ErrGroupInviteLinkUnauthorized, err)
} else if errors.Is(err, ErrIQNotFound) {
return "", wrapIQError(ErrGroupNotFound, err)
} else if errors.Is(err, ErrIQForbidden) {
return "", wrapIQError(ErrNotInGroup, err)
} else if err != nil {
return "", err
}
code, ok := resp.GetChildByTag("invite").Attrs["code"].(string)
if !ok {
return "", fmt.Errorf("didn't find invite code in response")
}
return InviteLinkPrefix + code, nil
}
// GetGroupInfoFromInvite gets the group info from an invite message.
//
// Note that this is specifically for invite messages, not invite links. Use GetGroupInfoFromLink for resolving chat.whatsapp.com links.
func (cli *Client) GetGroupInfoFromInvite(jid, inviter types.JID, code string, expiration int64) (*types.GroupInfo, error) {
resp, err := cli.sendGroupIQ(iqGet, jid, waBinary.Node{
Tag: "query",
Content: []waBinary.Node{{
Tag: "add_request",
Attrs: waBinary.Attrs{
"code": code,
"expiration": expiration,
"admin": inviter,
},
}},
})
if err != nil {
return nil, err
}
groupNode, ok := resp.GetOptionalChildByTag("group")
if !ok {
return nil, &ElementMissingError{Tag: "group", In: "response to invite group info query"}
}
return cli.parseGroupNode(&groupNode)
}
// JoinGroupWithInvite joins a group using an invite message.
//
// Note that this is specifically for invite messages, not invite links. Use JoinGroupWithLink for joining with chat.whatsapp.com links.
func (cli *Client) JoinGroupWithInvite(jid, inviter types.JID, code string, expiration int64) error {
_, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{
Tag: "accept",
Attrs: waBinary.Attrs{
"code": code,
"expiration": expiration,
"admin": inviter,
},
})
return err
}
// GetGroupInfoFromLink resolves the given invite link and asks the WhatsApp servers for info about the group.
// This will not cause the user to join the group.
func (cli *Client) GetGroupInfoFromLink(code string) (*types.GroupInfo, error) {
code = strings.TrimPrefix(code, InviteLinkPrefix)
resp, err := cli.sendGroupIQ(iqGet, types.GroupServerJID, waBinary.Node{
Tag: "invite",
Attrs: waBinary.Attrs{"code": code},
})
if errors.Is(err, ErrIQGone) {
return nil, wrapIQError(ErrInviteLinkRevoked, err)
} else if errors.Is(err, ErrIQNotAcceptable) {
return nil, wrapIQError(ErrInviteLinkInvalid, err)
} else if err != nil {
return nil, err
}
groupNode, ok := resp.GetOptionalChildByTag("group")
if !ok {
return nil, &ElementMissingError{Tag: "group", In: "response to group link info query"}
}
return cli.parseGroupNode(&groupNode)
}
// JoinGroupWithLink joins the group using the given invite link.
func (cli *Client) JoinGroupWithLink(code string) (types.JID, error) {
code = strings.TrimPrefix(code, InviteLinkPrefix)
resp, err := cli.sendGroupIQ(iqSet, types.GroupServerJID, waBinary.Node{
Tag: "invite",
Attrs: waBinary.Attrs{"code": code},
})
if errors.Is(err, ErrIQGone) {
return types.EmptyJID, wrapIQError(ErrInviteLinkRevoked, err)
} else if errors.Is(err, ErrIQNotAcceptable) {
return types.EmptyJID, wrapIQError(ErrInviteLinkInvalid, err)
} else if err != nil {
return types.EmptyJID, err
}
groupNode, ok := resp.GetOptionalChildByTag("group")
if !ok {
return types.EmptyJID, &ElementMissingError{Tag: "group", In: "response to group link join query"}
}
return groupNode.AttrGetter().JID("jid"), nil
}
// GetJoinedGroups returns the list of groups the user is participating in.
func (cli *Client) GetJoinedGroups() ([]*types.GroupInfo, error) {
resp, err := cli.sendGroupIQ(iqGet, types.GroupServerJID, waBinary.Node{
Tag: "participating",
Content: []waBinary.Node{
{Tag: "participants"},
{Tag: "description"},
},
})
if err != nil {
return nil, err
}
groups, ok := resp.GetOptionalChildByTag("groups")
if !ok {
return nil, &ElementMissingError{Tag: "groups", In: "response to group list query"}
}
children := groups.GetChildren()
infos := make([]*types.GroupInfo, 0, len(children))
for _, child := range children {
if child.Tag != "group" {
cli.Log.Debugf("Unexpected child in group list response: %s", child.XMLString())
continue
}
parsed, parseErr := cli.parseGroupNode(&child)
if parseErr != nil {
cli.Log.Warnf("Error parsing group %s: %v", parsed.JID, parseErr)
}
infos = append(infos, parsed)
}
return infos, 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(jid, true)
}
func (cli *Client) getGroupInfo(jid types.JID, lockParticipantCache bool) (*types.GroupInfo, error) {
res, err := cli.sendGroupIQ(iqGet, jid, waBinary.Node{
Tag: "query",
Attrs: waBinary.Attrs{"request": "interactive"},
})
if errors.Is(err, ErrIQNotFound) {
return nil, wrapIQError(ErrGroupNotFound, err)
} else if errors.Is(err, ErrIQForbidden) {
return nil, wrapIQError(ErrNotInGroup, err)
} else if err != nil {
return nil, err
}
groupNode, ok := res.GetOptionalChildByTag("group")
if !ok {
return nil, &ElementMissingError{Tag: "groups", In: "response to group info query"}
}
groupInfo, err := cli.parseGroupNode(&groupNode)
if err != nil {
return groupInfo, err
}
if lockParticipantCache {
cli.groupParticipantsCacheLock.Lock()
defer cli.groupParticipantsCacheLock.Unlock()
}
participants := make([]types.JID, len(groupInfo.Participants))
for i, part := range groupInfo.Participants {
participants[i] = part.JID
}
cli.groupParticipantsCache[jid] = participants
return groupInfo, nil
}
func (cli *Client) getGroupMembers(jid types.JID) ([]types.JID, error) {
cli.groupParticipantsCacheLock.Lock()
defer cli.groupParticipantsCacheLock.Unlock()
if _, ok := cli.groupParticipantsCache[jid]; !ok {
_, err := cli.getGroupInfo(jid, false)
if err != nil {
return nil, err
}
}
return cli.groupParticipantsCache[jid], nil
}
func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, error) {
var group types.GroupInfo
ag := groupNode.AttrGetter()
group.JID = types.NewJID(ag.String("id"), types.GroupServer)
group.OwnerJID = ag.OptionalJIDOrEmpty("creator")
group.Name = ag.String("subject")
group.NameSetAt = time.Unix(ag.Int64("s_t"), 0)
group.NameSetBy = ag.OptionalJIDOrEmpty("s_o")
group.GroupCreated = time.Unix(ag.Int64("creation"), 0)
group.AnnounceVersionID = ag.OptionalString("a_v_id")
group.ParticipantVersionID = ag.OptionalString("p_v_id")
for _, child := range groupNode.GetChildren() {
childAG := child.AttrGetter()
switch child.Tag {
case "participant":
pcpType := childAG.OptionalString("type")
participant := types.GroupParticipant{
IsAdmin: pcpType == "admin" || pcpType == "superadmin",
IsSuperAdmin: pcpType == "superadmin",
JID: childAG.JID("jid"),
}
group.Participants = append(group.Participants, participant)
case "description":
body, bodyOK := child.GetOptionalChildByTag("body")
if bodyOK {
group.Topic, _ = body.Content.(string)
group.TopicID = childAG.String("id")
group.TopicSetBy = childAG.OptionalJIDOrEmpty("participant")
group.TopicSetAt = time.Unix(childAG.Int64("t"), 0)
}
case "announcement":
group.IsAnnounce = true
case "locked":
group.IsLocked = true
case "ephemeral":
group.IsEphemeral = true
group.DisappearingTimer = uint32(childAG.Uint64("expiration"))
default:
cli.Log.Debugf("Unknown element in group node %s: %s", group.JID.String(), child.XMLString())
}
if !childAG.OK() {
cli.Log.Warnf("Possibly failed to parse %s element in group node: %+v", child.Tag, childAG.Errors)
}
}
return &group, ag.Error()
}
func parseParticipantList(node *waBinary.Node) (participants []types.JID) {
children := node.GetChildren()
participants = make([]types.JID, 0, len(children))
for _, child := range children {
jid, ok := child.Attrs["jid"].(types.JID)
if child.Tag != "participant" || !ok {
continue
}
participants = append(participants, jid)
}
return
}
func (cli *Client) parseGroupCreate(node *waBinary.Node) (*events.JoinedGroup, error) {
groupNode, ok := node.GetOptionalChildByTag("group")
if !ok {
return nil, fmt.Errorf("group create notification didn't contain group info")
}
var evt events.JoinedGroup
evt.Reason = node.AttrGetter().OptionalString("reason")
info, err := cli.parseGroupNode(&groupNode)
if err != nil {
return nil, fmt.Errorf("failed to parse group info in create notification: %w", err)
}
evt.GroupInfo = *info
return &evt, nil
}
func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, error) {
var evt events.GroupInfo
ag := node.AttrGetter()
evt.JID = ag.JID("from")
evt.Notify = ag.OptionalString("notify")
evt.Sender = ag.OptionalJID("participant")
evt.Timestamp = time.Unix(ag.Int64("t"), 0)
if !ag.OK() {
return nil, fmt.Errorf("group change doesn't contain required attributes: %w", ag.Error())
}
for _, child := range node.GetChildren() {
cag := child.AttrGetter()
if child.Tag == "add" || child.Tag == "remove" || child.Tag == "promote" || child.Tag == "demote" {
evt.PrevParticipantVersionID = cag.String("prev_v_id")
evt.ParticipantVersionID = cag.String("v_id")
}
switch child.Tag {
case "add":
evt.JoinReason = cag.OptionalString("reason")
evt.Join = parseParticipantList(&child)
case "remove":
evt.Leave = parseParticipantList(&child)
case "promote":
evt.Promote = parseParticipantList(&child)
case "demote":
evt.Demote = parseParticipantList(&child)
case "locked":
evt.Locked = &types.GroupLocked{IsLocked: true}
case "unlocked":
evt.Locked = &types.GroupLocked{IsLocked: false}
case "subject":
evt.Name = &types.GroupName{
Name: cag.String("subject"),
NameSetAt: time.Unix(cag.Int64("s_t"), 0),
NameSetBy: cag.OptionalJIDOrEmpty("s_o"),
}
case "description":
topicChild := child.GetChildByTag("body")
topicBytes, ok := topicChild.Content.([]byte)
if !ok {
return nil, fmt.Errorf("group change description has unexpected body: %s", topicChild.XMLString())
}
var setBy types.JID
if evt.Sender != nil {
setBy = *evt.Sender
}
evt.Topic = &types.GroupTopic{
Topic: string(topicBytes),
TopicID: cag.String("id"),
TopicSetAt: evt.Timestamp,
TopicSetBy: setBy,
}
case "announcement":
evt.Announce = &types.GroupAnnounce{
IsAnnounce: true,
AnnounceVersionID: cag.String("v_id"),
}
case "not_announcement":
evt.Announce = &types.GroupAnnounce{
IsAnnounce: false,
AnnounceVersionID: cag.String("v_id"),
}
case "invite":
link := InviteLinkPrefix + cag.String("code")
evt.NewInviteLink = &link
case "ephemeral":
timer := uint32(cag.Uint64("expiration"))
evt.Ephemeral = &types.GroupEphemeral{
IsEphemeral: true,
DisappearingTimer: timer,
}
case "not_ephemeral":
evt.Ephemeral = &types.GroupEphemeral{IsEphemeral: false}
default:
evt.UnknownChanges = append(evt.UnknownChanges, &child)
}
if !cag.OK() {
return nil, fmt.Errorf("group change %s element doesn't contain required attributes: %w", child.Tag, cag.Error())
}
}
return &evt, nil
}
func (cli *Client) updateGroupParticipantCache(evt *events.GroupInfo) {
if len(evt.Join) == 0 && len(evt.Leave) == 0 {
return
}
cli.groupParticipantsCacheLock.Lock()
defer cli.groupParticipantsCacheLock.Unlock()
cached, ok := cli.groupParticipantsCache[evt.JID]
if !ok {
return
}
Outer:
for _, jid := range evt.Join {
for _, existingJID := range cached {
if jid == existingJID {
continue Outer
}
}
cached = append(cached, jid)
}
for _, jid := range evt.Leave {
for i, existingJID := range cached {
if existingJID == jid {
cached[i] = cached[len(cached)-1]
cached = cached[:len(cached)-1]
break
}
}
}
cli.groupParticipantsCache[evt.JID] = cached
}
func (cli *Client) parseGroupNotification(node *waBinary.Node) (interface{}, error) {
children := node.GetChildren()
if len(children) == 1 && children[0].Tag == "create" {
return cli.parseGroupCreate(&children[0])
} else {
groupChange, err := cli.parseGroupChange(node)
if err != nil {
return nil, err
}
cli.updateGroupParticipantCache(groupChange)
return groupChange, nil
}
}

131
vendor/go.mau.fi/whatsmeow/handshake.go vendored Normal file
View File

@ -0,0 +1,131 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"bytes"
"fmt"
"time"
"google.golang.org/protobuf/proto"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/socket"
"go.mau.fi/whatsmeow/util/keys"
)
const NoiseHandshakeResponseTimeout = 20 * time.Second
// doHandshake implements the Noise_XX_25519_AESGCM_SHA256 handshake for the WhatsApp web API.
func (cli *Client) doHandshake(fs *socket.FrameSocket, ephemeralKP keys.KeyPair) error {
nh := socket.NewNoiseHandshake()
nh.Start(socket.NoiseStartPattern, fs.Header)
nh.Authenticate(ephemeralKP.Pub[:])
data, err := proto.Marshal(&waProto.HandshakeMessage{
ClientHello: &waProto.ClientHello{
Ephemeral: ephemeralKP.Pub[:],
},
})
if err != nil {
return fmt.Errorf("failed to marshal handshake message: %w", err)
}
err = fs.SendFrame(data)
if err != nil {
return fmt.Errorf("failed to send handshake message: %w", err)
}
var resp []byte
select {
case resp = <-fs.Frames:
case <-time.After(NoiseHandshakeResponseTimeout):
return fmt.Errorf("timed out waiting for handshake response")
}
var handshakeResponse waProto.HandshakeMessage
err = proto.Unmarshal(resp, &handshakeResponse)
if err != nil {
return fmt.Errorf("failed to unmarshal handshake response: %w", err)
}
serverEphemeral := handshakeResponse.GetServerHello().GetEphemeral()
serverStaticCiphertext := handshakeResponse.GetServerHello().GetStatic()
certificateCiphertext := handshakeResponse.GetServerHello().GetPayload()
if len(serverEphemeral) != 32 || serverStaticCiphertext == nil || certificateCiphertext == nil {
return fmt.Errorf("missing parts of handshake response")
}
serverEphemeralArr := *(*[32]byte)(serverEphemeral)
nh.Authenticate(serverEphemeral)
err = nh.MixSharedSecretIntoKey(*ephemeralKP.Priv, serverEphemeralArr)
if err != nil {
return fmt.Errorf("failed to mix server ephemeral key in: %w", err)
}
staticDecrypted, err := nh.Decrypt(serverStaticCiphertext)
if err != nil {
return fmt.Errorf("failed to decrypt server static ciphertext: %w", err)
} else if len(staticDecrypted) != 32 {
return fmt.Errorf("unexpected length of server static plaintext %d (expected 32)", len(staticDecrypted))
}
err = nh.MixSharedSecretIntoKey(*ephemeralKP.Priv, *(*[32]byte)(staticDecrypted))
if err != nil {
return fmt.Errorf("failed to mix server static key in: %w", err)
}
certDecrypted, err := nh.Decrypt(certificateCiphertext)
if err != nil {
return fmt.Errorf("failed to decrypt noise certificate ciphertext: %w", err)
}
var cert waProto.NoiseCertificate
err = proto.Unmarshal(certDecrypted, &cert)
if err != nil {
return fmt.Errorf("failed to unmarshal noise certificate: %w", err)
}
certDetailsRaw := cert.GetDetails()
certSignature := cert.GetSignature()
if certDetailsRaw == nil || certSignature == nil {
return fmt.Errorf("missing parts of noise certificate")
}
var certDetails waProto.NoiseCertificateDetails
err = proto.Unmarshal(certDetailsRaw, &certDetails)
if err != nil {
return fmt.Errorf("failed to unmarshal noise certificate details: %w", err)
} else if !bytes.Equal(certDetails.GetKey(), staticDecrypted) {
return fmt.Errorf("cert key doesn't match decrypted static")
}
encryptedPubkey := nh.Encrypt(cli.Store.NoiseKey.Pub[:])
err = nh.MixSharedSecretIntoKey(*cli.Store.NoiseKey.Priv, serverEphemeralArr)
if err != nil {
return fmt.Errorf("failed to mix noise private key in: %w", err)
}
clientFinishPayloadBytes, err := proto.Marshal(cli.Store.GetClientPayload())
if err != nil {
return fmt.Errorf("failed to marshal client finish payload: %w", err)
}
encryptedClientFinishPayload := nh.Encrypt(clientFinishPayloadBytes)
data, err = proto.Marshal(&waProto.HandshakeMessage{
ClientFinish: &waProto.ClientFinish{
Static: encryptedPubkey,
Payload: encryptedClientFinishPayload,
},
})
if err != nil {
return fmt.Errorf("failed to marshal handshake finish message: %w", err)
}
err = fs.SendFrame(data)
if err != nil {
return fmt.Errorf("failed to send handshake finish message: %w", err)
}
ns, err := nh.Finish(fs, cli.handleFrame, cli.onDisconnect)
if err != nil {
return fmt.Errorf("failed to create noise socket: %w", err)
}
cli.socket = ns
return nil
}

62
vendor/go.mau.fi/whatsmeow/keepalive.go vendored Normal file
View File

@ -0,0 +1,62 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"math/rand"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
var (
// KeepAliveResponseDeadline specifies the duration to wait for a response to websocket keepalive pings.
KeepAliveResponseDeadline = 10 * time.Second
// KeepAliveIntervalMin specifies the minimum interval for websocket keepalive pings.
KeepAliveIntervalMin = 20 * time.Second
// KeepAliveIntervalMax specifies the maximum interval for websocket keepalive pings.
KeepAliveIntervalMax = 30 * time.Second
)
func (cli *Client) keepAliveLoop(ctx context.Context) {
for {
interval := rand.Int63n(KeepAliveIntervalMax.Milliseconds()-KeepAliveIntervalMin.Milliseconds()) + KeepAliveIntervalMin.Milliseconds()
select {
case <-time.After(time.Duration(interval) * time.Millisecond):
if !cli.sendKeepAlive(ctx) {
return
}
case <-ctx.Done():
return
}
}
}
func (cli *Client) sendKeepAlive(ctx context.Context) bool {
respCh, err := cli.sendIQAsync(infoQuery{
Namespace: "w:p",
Type: "get",
To: types.ServerJID,
Content: []waBinary.Node{{Tag: "ping"}},
})
if err != nil {
cli.Log.Warnf("Failed to send keepalive: %v", err)
return true
}
select {
case <-respCh:
// All good
case <-time.After(KeepAliveResponseDeadline):
// TODO disconnect websocket?
cli.Log.Warnf("Keepalive timed out")
case <-ctx.Done():
return false
}
return true
}

93
vendor/go.mau.fi/whatsmeow/mediaconn.go vendored Normal file
View File

@ -0,0 +1,93 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"fmt"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
//type MediaConnIP struct {
// IP4 net.IP
// IP6 net.IP
//}
// MediaConnHost represents a single host to download media from.
type MediaConnHost struct {
Hostname string
//IPs []MediaConnIP
}
// MediaConn contains a list of WhatsApp servers from which attachments can be downloaded from.
type MediaConn struct {
Auth string
AuthTTL int
TTL int
MaxBuckets int
FetchedAt time.Time
Hosts []MediaConnHost
}
// Expiry returns the time when the MediaConn expires.
func (mc *MediaConn) Expiry() time.Time {
return mc.FetchedAt.Add(time.Duration(mc.TTL) * time.Second)
}
func (cli *Client) refreshMediaConn(force bool) error {
cli.mediaConnLock.Lock()
defer cli.mediaConnLock.Unlock()
if cli.mediaConn == nil || force || time.Now().After(cli.mediaConn.Expiry()) {
var err error
cli.mediaConn, err = cli.queryMediaConn()
if err != nil {
return err
}
}
return nil
}
func (cli *Client) queryMediaConn() (*MediaConn, error) {
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:m",
Type: "set",
To: types.ServerJID,
Content: []waBinary.Node{{Tag: "media_conn"}},
})
if err != nil {
return nil, fmt.Errorf("failed to query media connections: %w", err)
} else if len(resp.GetChildren()) == 0 || resp.GetChildren()[0].Tag != "media_conn" {
return nil, fmt.Errorf("failed to query media connections: unexpected child tag")
}
respMC := resp.GetChildren()[0]
var mc MediaConn
ag := respMC.AttrGetter()
mc.FetchedAt = time.Now()
mc.Auth = ag.String("auth")
mc.TTL = ag.Int("ttl")
mc.AuthTTL = ag.Int("auth_ttl")
mc.MaxBuckets = ag.Int("max_buckets")
if !ag.OK() {
return nil, fmt.Errorf("failed to parse media connections: %+v", ag.Errors)
}
for _, child := range respMC.GetChildren() {
if child.Tag != "host" {
cli.Log.Warnf("Unexpected child in media_conn element: %s", child.XMLString())
continue
}
cag := child.AttrGetter()
mc.Hosts = append(mc.Hosts, MediaConnHost{
Hostname: cag.String("hostname"),
})
if !cag.OK() {
return nil, fmt.Errorf("failed to parse media connection host: %+v", ag.Errors)
}
}
return &mc, nil
}

383
vendor/go.mau.fi/whatsmeow/message.go vendored Normal file
View File

@ -0,0 +1,383 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"bytes"
"compress/zlib"
"crypto/rand"
"errors"
"fmt"
"io"
"strconv"
"time"
"go.mau.fi/libsignal/signalerror"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/protocol"
"go.mau.fi/libsignal/session"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
var pbSerializer = store.SignalProtobufSerializer
func (cli *Client) handleEncryptedMessage(node *waBinary.Node) {
info, err := cli.parseMessageInfo(node)
if err != nil {
cli.Log.Warnf("Failed to parse message: %v", err)
} else {
if len(info.PushName) > 0 && info.PushName != "-" {
go cli.updatePushName(info.Sender, info, info.PushName)
}
cli.decryptMessages(info, node)
}
}
func (cli *Client) parseMessageSource(node *waBinary.Node) (source types.MessageSource, err error) {
from, ok := node.Attrs["from"].(types.JID)
if !ok {
err = fmt.Errorf("didn't find valid `from` attribute in message")
} else if from.Server == types.GroupServer || from.Server == types.BroadcastServer {
source.IsGroup = true
source.Chat = from
sender, ok := node.Attrs["participant"].(types.JID)
if !ok {
err = fmt.Errorf("didn't find valid `participant` attribute in group message")
} else {
source.Sender = sender
if source.Sender.User == cli.Store.ID.User {
source.IsFromMe = true
}
}
if from.Server == types.BroadcastServer {
recipient, ok := node.Attrs["recipient"].(types.JID)
if ok {
source.BroadcastListOwner = recipient
}
}
} else if from.User == cli.Store.ID.User {
source.IsFromMe = true
source.Sender = from
recipient, ok := node.Attrs["recipient"].(types.JID)
if !ok {
source.Chat = from.ToNonAD()
} else {
source.Chat = recipient
}
} else {
source.Chat = from.ToNonAD()
source.Sender = from
}
return
}
func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, error) {
var info types.MessageInfo
var err error
var ok bool
info.MessageSource, err = cli.parseMessageSource(node)
if err != nil {
return nil, err
}
info.ID, ok = node.Attrs["id"].(string)
if !ok {
return nil, fmt.Errorf("didn't find valid `id` attribute in message")
}
ts, ok := node.Attrs["t"].(string)
if !ok {
return nil, fmt.Errorf("didn't find valid `t` (timestamp) attribute in message")
}
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return nil, fmt.Errorf("didn't find valid `t` (timestamp) attribute in message: %w", err)
}
info.Timestamp = time.Unix(tsInt, 0)
info.PushName, _ = node.Attrs["notify"].(string)
info.Category, _ = node.Attrs["category"].(string)
return &info, nil
}
func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node) {
go cli.sendAck(node)
if len(node.GetChildrenByTag("unavailable")) == len(node.GetChildren()) {
cli.Log.Warnf("Unavailable message %s from %s", info.ID, info.SourceString())
go cli.sendRetryReceipt(node, true)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: true})
return
}
children := node.GetChildren()
cli.Log.Debugf("Decrypting %d messages from %s", len(children), info.SourceString())
handled := false
for _, child := range children {
if child.Tag != "enc" {
continue
}
encType, ok := child.Attrs["type"].(string)
if !ok {
continue
}
var decrypted []byte
var err error
if encType == "pkmsg" || encType == "msg" {
decrypted, err = cli.decryptDM(&child, info.Sender, encType == "pkmsg")
} else if info.IsGroup && encType == "skmsg" {
decrypted, err = cli.decryptGroupMsg(&child, info.Sender, info.Chat)
} else {
cli.Log.Warnf("Unhandled encrypted message (type %s) from %s", encType, info.SourceString())
continue
}
if err != nil {
cli.Log.Warnf("Error decrypting message from %s: %v", info.SourceString(), err)
go cli.sendRetryReceipt(node, false)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: false})
return
}
var msg waProto.Message
err = proto.Unmarshal(decrypted, &msg)
if err != nil {
cli.Log.Warnf("Error unmarshaling decrypted message from %s: %v", info.SourceString(), err)
continue
}
cli.handleDecryptedMessage(info, &msg)
handled = true
}
if handled {
go cli.sendMessageReceipt(info)
}
}
func (cli *Client) decryptDM(child *waBinary.Node, from types.JID, isPreKey bool) ([]byte, error) {
content, _ := child.Content.([]byte)
builder := session.NewBuilderFromSignal(cli.Store, from.SignalAddress(), pbSerializer)
cipher := session.NewCipher(builder, from.SignalAddress())
var plaintext []byte
if isPreKey {
preKeyMsg, err := protocol.NewPreKeySignalMessageFromBytes(content, pbSerializer.PreKeySignalMessage, pbSerializer.SignalMessage)
if err != nil {
return nil, fmt.Errorf("failed to parse prekey message: %w", err)
}
plaintext, _, err = cipher.DecryptMessageReturnKey(preKeyMsg)
if errors.Is(err, signalerror.ErrUntrustedIdentity) {
cli.Log.Warnf("Got %v error while trying to decrypt prekey message from %s, clearing stored identity and retrying", err, from)
err = cli.Store.Identities.DeleteIdentity(from.SignalAddress().String())
if err != nil {
cli.Log.Warnf("Failed to delete identity of %s from store after decryption error: %v", from, err)
}
err = cli.Store.Sessions.DeleteSession(from.SignalAddress().String())
if err != nil {
cli.Log.Warnf("Failed to delete session with %s from store after decryption error: %v", from, err)
}
cli.dispatchEvent(&events.IdentityChange{JID: from, Timestamp: time.Now(), Implicit: true})
plaintext, _, err = cipher.DecryptMessageReturnKey(preKeyMsg)
}
if err != nil {
return nil, fmt.Errorf("failed to decrypt prekey message: %w", err)
}
} else {
msg, err := protocol.NewSignalMessageFromBytes(content, pbSerializer.SignalMessage)
if err != nil {
return nil, fmt.Errorf("failed to parse normal message: %w", err)
}
plaintext, err = cipher.Decrypt(msg)
if err != nil {
return nil, fmt.Errorf("failed to decrypt normal message: %w", err)
}
}
return unpadMessage(plaintext)
}
func (cli *Client) decryptGroupMsg(child *waBinary.Node, from types.JID, chat types.JID) ([]byte, error) {
content, _ := child.Content.([]byte)
senderKeyName := protocol.NewSenderKeyName(chat.String(), from.SignalAddress())
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
cipher := groups.NewGroupCipher(builder, senderKeyName, cli.Store)
msg, err := protocol.NewSenderKeyMessageFromBytes(content, pbSerializer.SenderKeyMessage)
if err != nil {
return nil, fmt.Errorf("failed to parse group message: %w", err)
}
plaintext, err := cipher.Decrypt(msg)
if err != nil {
return nil, fmt.Errorf("failed to decrypt group message: %w", err)
}
return unpadMessage(plaintext)
}
const checkPadding = true
func isValidPadding(plaintext []byte) bool {
lastByte := plaintext[len(plaintext)-1]
expectedPadding := bytes.Repeat([]byte{lastByte}, int(lastByte))
return bytes.HasSuffix(plaintext, expectedPadding)
}
func unpadMessage(plaintext []byte) ([]byte, error) {
if checkPadding && !isValidPadding(plaintext) {
return nil, fmt.Errorf("plaintext doesn't have expected padding")
}
return plaintext[:len(plaintext)-int(plaintext[len(plaintext)-1])], nil
}
func padMessage(plaintext []byte) []byte {
var pad [1]byte
_, err := rand.Read(pad[:])
if err != nil {
panic(err)
}
pad[0] &= 0xf
if pad[0] == 0 {
pad[0] = 0xf
}
plaintext = append(plaintext, bytes.Repeat(pad[:], int(pad[0]))...)
return plaintext
}
func (cli *Client) handleSenderKeyDistributionMessage(chat, from types.JID, rawSKDMsg *waProto.SenderKeyDistributionMessage) {
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(chat.String(), from.SignalAddress())
sdkMsg, err := protocol.NewSenderKeyDistributionMessageFromBytes(rawSKDMsg.AxolotlSenderKeyDistributionMessage, pbSerializer.SenderKeyDistributionMessage)
if err != nil {
cli.Log.Errorf("Failed to parse sender key distribution message from %s for %s: %v", from, chat, err)
return
}
builder.Process(senderKeyName, sdkMsg)
cli.Log.Debugf("Processed sender key distribution message from %s in %s", senderKeyName.Sender().String(), senderKeyName.GroupID())
}
func (cli *Client) handleHistorySyncNotification(notif *waProto.HistorySyncNotification) {
var historySync waProto.HistorySync
if data, err := cli.Download(notif); err != nil {
cli.Log.Errorf("Failed to download history sync data: %v", err)
} else if reader, err := zlib.NewReader(bytes.NewReader(data)); err != nil {
cli.Log.Errorf("Failed to create zlib reader for history sync data: %v", err)
} else if rawData, err := io.ReadAll(reader); err != nil {
cli.Log.Errorf("Failed to decompress history sync data: %v", err)
} else if err = proto.Unmarshal(rawData, &historySync); err != nil {
cli.Log.Errorf("Failed to unmarshal history sync data: %v", err)
} else {
cli.Log.Debugf("Received history sync")
if historySync.GetSyncType() == waProto.HistorySync_PUSH_NAME {
go cli.handleHistoricalPushNames(historySync.GetPushnames())
}
cli.dispatchEvent(&events.HistorySync{
Data: &historySync,
})
}
}
func (cli *Client) handleAppStateSyncKeyShare(keys *waProto.AppStateSyncKeyShare) {
for _, key := range keys.GetKeys() {
marshaledFingerprint, err := proto.Marshal(key.GetKeyData().GetFingerprint())
if err != nil {
cli.Log.Errorf("Failed to marshal fingerprint of app state sync key %X", key.GetKeyId().GetKeyId())
continue
}
err = cli.Store.AppStateKeys.PutAppStateSyncKey(key.GetKeyId().GetKeyId(), store.AppStateSyncKey{
Data: key.GetKeyData().GetKeyData(),
Fingerprint: marshaledFingerprint,
Timestamp: key.GetKeyData().GetTimestamp(),
})
if err != nil {
cli.Log.Errorf("Failed to store app state sync key %X", key.GetKeyId().GetKeyId())
continue
}
cli.Log.Debugf("Received app state sync key %X", key.GetKeyId().GetKeyId())
}
for _, name := range appstate.AllPatchNames {
err := cli.FetchAppState(name, false, true)
if err != nil {
cli.Log.Errorf("Failed to do initial fetch of app state %s: %v", name, err)
}
}
}
func (cli *Client) handleProtocolMessage(info *types.MessageInfo, msg *waProto.Message) {
protoMsg := msg.GetProtocolMessage()
if protoMsg.GetHistorySyncNotification() != nil && info.IsFromMe {
cli.handleHistorySyncNotification(protoMsg.HistorySyncNotification)
cli.sendProtocolMessageReceipt(info.ID, "hist_sync")
}
if protoMsg.GetAppStateSyncKeyShare() != nil && info.IsFromMe {
cli.handleAppStateSyncKeyShare(protoMsg.AppStateSyncKeyShare)
}
if info.Category == "peer" {
cli.sendProtocolMessageReceipt(info.ID, "peer_msg")
}
}
func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.Message) {
evt := &events.Message{Info: *info, RawMessage: msg}
// First unwrap device sent messages
if msg.GetDeviceSentMessage().GetMessage() != nil {
msg = msg.GetDeviceSentMessage().GetMessage()
evt.Info.DeviceSentMeta = &types.DeviceSentMeta{
DestinationJID: msg.GetDeviceSentMessage().GetDestinationJid(),
Phash: msg.GetDeviceSentMessage().GetPhash(),
}
}
if msg.GetSenderKeyDistributionMessage() != nil {
if !info.IsGroup {
cli.Log.Warnf("Got sender key distribution message in non-group chat from", info.Sender)
} else {
cli.handleSenderKeyDistributionMessage(info.Chat, info.Sender, msg.SenderKeyDistributionMessage)
}
}
if msg.GetProtocolMessage() != nil {
go cli.handleProtocolMessage(info, msg)
}
// Unwrap ephemeral and view-once messages
// Hopefully sender key distribution messages and protocol messages can't be inside ephemeral messages
if msg.GetEphemeralMessage().GetMessage() != nil {
msg = msg.GetEphemeralMessage().GetMessage()
evt.IsEphemeral = true
}
if msg.GetViewOnceMessage().GetMessage() != nil {
msg = msg.GetViewOnceMessage().GetMessage()
evt.IsViewOnce = true
}
evt.Message = msg
cli.dispatchEvent(evt)
}
func (cli *Client) sendProtocolMessageReceipt(id, msgType string) {
if len(id) == 0 {
return
}
err := cli.sendNode(waBinary.Node{
Tag: "receipt",
Attrs: waBinary.Attrs{
"id": id,
"type": msgType,
"to": types.NewJID(cli.Store.ID.User, types.LegacyUserServer),
},
Content: nil,
})
if err != nil {
cli.Log.Warnf("Failed to send acknowledgement for protocol message %s: %v", id, err)
}
}

View File

@ -0,0 +1,205 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"errors"
"time"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
func (cli *Client) handleEncryptNotification(node *waBinary.Node) {
from := node.AttrGetter().JID("from")
if from == types.ServerJID {
count := node.GetChildByTag("count")
ag := count.AttrGetter()
otksLeft := ag.Int("value")
if !ag.OK() {
cli.Log.Warnf("Didn't get number of OTKs left in encryption notification %s", node.XMLString())
return
}
cli.Log.Infof("Got prekey count from server: %s", node.XMLString())
if otksLeft < MinPreKeyCount {
cli.uploadPreKeys()
}
} else if _, ok := node.GetOptionalChildByTag("identity"); ok {
cli.Log.Debugf("Got identity change for %s: %s, deleting all identities/sessions for that number", from, node.XMLString())
err := cli.Store.Identities.DeleteAllIdentities(from.User)
if err != nil {
cli.Log.Warnf("Failed to delete all identities of %s from store after identity change: %v", from, err)
}
err = cli.Store.Sessions.DeleteAllSessions(from.User)
if err != nil {
cli.Log.Warnf("Failed to delete all sessions of %s from store after identity change: %v", from, err)
}
ts := time.Unix(node.AttrGetter().Int64("t"), 0)
cli.dispatchEvent(&events.IdentityChange{JID: from, Timestamp: ts})
} else {
cli.Log.Debugf("Got unknown encryption notification from server: %s", node.XMLString())
}
}
func (cli *Client) handleAppStateNotification(node *waBinary.Node) {
for _, collection := range node.GetChildrenByTag("collection") {
ag := collection.AttrGetter()
name := appstate.WAPatchName(ag.String("name"))
version := ag.Uint64("version")
cli.Log.Debugf("Got server sync notification that app state %s has updated to version %d", name, version)
err := cli.FetchAppState(name, false, false)
if errors.Is(err, ErrIQDisconnected) || errors.Is(err, ErrNotConnected) {
// There are some app state changes right before a remote logout, so stop syncing if we're disconnected.
cli.Log.Debugf("Failed to sync app state after notification: %v, not trying to sync other states", err)
return
} else if err != nil {
cli.Log.Errorf("Failed to sync app state after notification: %v", err)
}
}
}
func (cli *Client) handlePictureNotification(node *waBinary.Node) {
ts := time.Unix(node.AttrGetter().Int64("t"), 0)
for _, child := range node.GetChildren() {
ag := child.AttrGetter()
var evt events.Picture
evt.Timestamp = ts
evt.JID = ag.JID("jid")
evt.Author = ag.OptionalJIDOrEmpty("author")
if child.Tag == "delete" {
evt.Remove = true
} else if child.Tag == "add" {
evt.PictureID = ag.String("id")
} else if child.Tag == "set" {
// TODO sometimes there's a hash and no ID?
evt.PictureID = ag.String("id")
} else {
continue
}
if !ag.OK() {
cli.Log.Debugf("Ignoring picture change notification with unexpected attributes: %v", ag.Error())
continue
}
cli.dispatchEvent(&evt)
}
}
func (cli *Client) handleDeviceNotification(node *waBinary.Node) {
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
ag := node.AttrGetter()
from := ag.JID("from")
cached, ok := cli.userDevicesCache[from]
if !ok {
cli.Log.Debugf("No device list cached for %s, ignoring device list notification", from)
return
}
cachedParticipantHash := participantListHashV2(cached)
for _, child := range node.GetChildren() {
if child.Tag != "add" && child.Tag != "remove" {
cli.Log.Debugf("Unknown device list change tag %s", child.Tag)
continue
}
cag := child.AttrGetter()
deviceHash := cag.String("device_hash")
deviceChild, _ := child.GetOptionalChildByTag("device")
changedDeviceJID := deviceChild.AttrGetter().JID("jid")
switch child.Tag {
case "add":
cached = append(cached, changedDeviceJID)
case "remove":
for i, jid := range cached {
if jid == changedDeviceJID {
cached = append(cached[:i], cached[i+1:]...)
}
}
case "update":
// ???
}
newParticipantHash := participantListHashV2(cached)
if newParticipantHash == deviceHash {
cli.Log.Debugf("%s's device list hash changed from %s to %s (%s). New hash matches", from, cachedParticipantHash, deviceHash, child.Tag)
cli.userDevicesCache[from] = cached
} else {
cli.Log.Warnf("%s's device list hash changed from %s to %s (%s). New hash doesn't match (%s)", from, cachedParticipantHash, deviceHash, child.Tag, newParticipantHash)
delete(cli.userDevicesCache, from)
}
}
}
func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) {
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
cached, ok := cli.userDevicesCache[cli.Store.ID.ToNonAD()]
if !ok {
cli.Log.Debugf("Ignoring own device change notification, device list not cached")
return
}
oldHash := participantListHashV2(cached)
expectedNewHash := node.AttrGetter().String("dhash")
var newDeviceList []types.JID
for _, child := range node.GetChildren() {
jid := child.AttrGetter().JID("jid")
if child.Tag == "device" && !jid.IsEmpty() {
jid.AD = true
newDeviceList = append(newDeviceList, jid)
}
}
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())
} else {
cli.Log.Debugf("Received own device list change notification %s -> %s", oldHash, newHash)
cli.userDevicesCache[cli.Store.ID.ToNonAD()] = newDeviceList
}
}
func (cli *Client) handleAccountSyncNotification(node *waBinary.Node) {
for _, child := range node.GetChildren() {
switch child.Tag {
case "privacy":
cli.handlePrivacySettingsNotification(&child)
case "devices":
cli.handleOwnDevicesNotification(&child)
default:
cli.Log.Debugf("Unhandled account sync item %s", child.Tag)
}
}
}
func (cli *Client) handleNotification(node *waBinary.Node) {
ag := node.AttrGetter()
notifType := ag.String("type")
if !ag.OK() {
return
}
go cli.sendAck(node)
switch notifType {
case "encrypt":
go cli.handleEncryptNotification(node)
case "server_sync":
go cli.handleAppStateNotification(node)
case "account_sync":
go cli.handleAccountSyncNotification(node)
case "devices":
go cli.handleDeviceNotification(node)
case "w:gp2":
evt, err := cli.parseGroupNotification(node)
if err != nil {
cli.Log.Errorf("Failed to parse group notification: %v", err)
} else {
go cli.dispatchEvent(evt)
}
case "picture":
go cli.handlePictureNotification(node)
default:
cli.Log.Debugf("Unhandled notification with type %s", notifType)
}
}

244
vendor/go.mau.fi/whatsmeow/pair.go vendored Normal file
View File

@ -0,0 +1,244 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/ecc"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/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 {
return
}
switch children[0].Tag {
case "pair-device":
cli.handlePairDevice(node)
case "pair-success":
cli.handlePairSuccess(node)
}
}
func (cli *Client) handlePairDevice(node *waBinary.Node) {
pairDevice := node.GetChildByTag("pair-device")
err := cli.sendNode(waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": node.Attrs["from"],
"id": node.Attrs["id"],
"type": "result",
},
})
if err != nil {
cli.Log.Warnf("Failed to send acknowledgement for pair-device request: %v", err)
}
evt := &events.QR{Codes: make([]string, 0, len(pairDevice.GetChildren()))}
for i, child := range pairDevice.GetChildren() {
if child.Tag != "ref" {
cli.Log.Warnf("pair-device node contains unexpected child tag %s at index %d", child.Tag, i)
continue
}
content, ok := child.Content.([]byte)
if !ok {
cli.Log.Warnf("pair-device node contains unexpected child content type %T at index %d", child, i)
continue
}
evt.Codes = append(evt.Codes, cli.makeQRData(string(content)))
}
cli.dispatchEvent(evt)
}
func (cli *Client) makeQRData(ref string) string {
noise := base64.StdEncoding.EncodeToString(cli.Store.NoiseKey.Pub[:])
identity := base64.StdEncoding.EncodeToString(cli.Store.IdentityKey.Pub[:])
adv := base64.StdEncoding.EncodeToString(cli.Store.AdvSecretKey)
return strings.Join([]string{ref, noise, identity, adv}, ",")
}
func (cli *Client) handlePairSuccess(node *waBinary.Node) {
id := node.Attrs["id"].(string)
pairSuccess := node.GetChildByTag("pair-success")
deviceIdentityBytes, _ := pairSuccess.GetChildByTag("device-identity").Content.([]byte)
businessName, _ := pairSuccess.GetChildByTag("biz").Attrs["name"].(string)
jid, _ := pairSuccess.GetChildByTag("device").Attrs["jid"].(types.JID)
platform, _ := pairSuccess.GetChildByTag("platform").Attrs["name"].(string)
go func() {
err := cli.handlePair(deviceIdentityBytes, id, businessName, platform, jid)
if err != nil {
cli.Log.Errorf("Failed to pair device: %v", err)
cli.Disconnect()
cli.dispatchEvent(&events.PairError{ID: jid, BusinessName: businessName, Platform: platform, Error: err})
} else {
cli.Log.Infof("Successfully paired %s", cli.Store.ID)
cli.dispatchEvent(&events.PairSuccess{ID: jid, BusinessName: businessName, Platform: platform})
}
}()
}
func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, platform string, jid types.JID) error {
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)
}
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")
}
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)
}
if !verifyDeviceIdentityAccountSignature(&deviceIdentity, cli.Store.IdentityKey) {
cli.sendIQError(reqID, 401, "not-authorized")
return fmt.Errorf("invalid device signature in pair success message")
}
deviceIdentity.DeviceSignature = generateDeviceSignature(&deviceIdentity, cli.Store.IdentityKey)[:]
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)
}
mainDeviceJID := jid
mainDeviceJID.Device = 0
mainDeviceIdentity := *(*[32]byte)(deviceIdentity.AccountSignatureKey)
deviceIdentity.AccountSignatureKey = nil
cli.Store.Account = proto.Clone(&deviceIdentity).(*waProto.ADVSignedDeviceIdentity)
selfSignedDeviceIdentity, err := proto.Marshal(&deviceIdentity)
if err != nil {
cli.sendIQError(reqID, 500, "internal-error")
return fmt.Errorf("failed to marshal self-signed device identity: %w", err)
}
cli.Store.ID = &jid
cli.Store.BusinessName = businessName
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)
}
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)
}
// Expect a disconnect after this and don't dispatch the usual Disconnected event
cli.expectDisconnect()
err = cli.sendNode(waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": types.ServerJID,
"type": "result",
"id": reqID,
},
Content: []waBinary.Node{{
Tag: "pair-device-sign",
Content: []waBinary.Node{{
Tag: "device-identity",
Attrs: waBinary.Attrs{
"key-index": deviceIdentityDetails.GetKeyIndex(),
},
Content: selfSignedDeviceIdentity,
}},
}},
})
if err != nil {
_ = cli.Store.Delete()
return fmt.Errorf("failed to send pairing confirmation: %w", err)
}
return nil
}
func concatBytes(data ...[]byte) []byte {
length := 0
for _, item := range data {
length += len(item)
}
output := make([]byte, length)
ptr := 0
for _, item := range data {
ptr += copy(output[ptr:ptr+len(item)], item)
}
return output
}
func verifyDeviceIdentityAccountSignature(deviceIdentity *waProto.ADVSignedDeviceIdentity, ikp *keys.KeyPair) bool {
if len(deviceIdentity.AccountSignatureKey) != 32 || len(deviceIdentity.AccountSignature) != 64 {
return false
}
signatureKey := ecc.NewDjbECPublicKey(*(*[32]byte)(deviceIdentity.AccountSignatureKey))
signature := *(*[64]byte)(deviceIdentity.AccountSignature)
message := concatBytes([]byte{6, 0}, deviceIdentity.Details, ikp.Pub[:])
return ecc.VerifySignature(signatureKey, message, signature)
}
func generateDeviceSignature(deviceIdentity *waProto.ADVSignedDeviceIdentity, ikp *keys.KeyPair) *[64]byte {
message := concatBytes([]byte{6, 1}, deviceIdentity.Details, ikp.Pub[:], deviceIdentity.AccountSignatureKey)
sig := ecc.CalculateSignature(ecc.NewDjbECPrivateKey(*ikp.Priv), message)
return &sig
}
func (cli *Client) sendIQError(id string, code int, text string) waBinary.Node {
return waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": types.ServerJID,
"type": "error",
"id": id,
},
Content: []waBinary.Node{{
Tag: "error",
Attrs: waBinary.Attrs{
"code": code,
"text": text,
},
}},
}
}

235
vendor/go.mau.fi/whatsmeow/prekeys.go vendored Normal file
View File

@ -0,0 +1,235 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"encoding/binary"
"fmt"
"time"
"go.mau.fi/libsignal/ecc"
"go.mau.fi/libsignal/keys/identity"
"go.mau.fi/libsignal/keys/prekey"
"go.mau.fi/libsignal/util/optional"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
)
const (
// WantedPreKeyCount is the number of prekeys that the client should upload to the WhatsApp servers in a single batch.
WantedPreKeyCount = 50
// MinPreKeyCount is the number of prekeys when the client will upload a new batch of prekeys to the WhatsApp servers.
MinPreKeyCount = 5
)
func (cli *Client) getServerPreKeyCount() (int, error) {
resp, err := cli.sendIQ(infoQuery{
Namespace: "encrypt",
Type: "get",
To: types.ServerJID,
Content: []waBinary.Node{
{Tag: "count"},
},
})
if err != nil {
return 0, fmt.Errorf("failed to get prekey count on server: %w", err)
}
count := resp.GetChildByTag("count")
ag := count.AttrGetter()
val := ag.Int("value")
return val, ag.Error()
}
func (cli *Client) uploadPreKeys() {
cli.uploadPreKeysLock.Lock()
defer cli.uploadPreKeysLock.Unlock()
if cli.lastPreKeyUpload.Add(10 * time.Minute).After(time.Now()) {
sc, _ := cli.getServerPreKeyCount()
if sc >= WantedPreKeyCount {
cli.Log.Debugf("Canceling prekey upload request due to likely race condition")
return
}
}
var registrationIDBytes [4]byte
binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID)
preKeys, err := cli.Store.PreKeys.GetOrGenPreKeys(WantedPreKeyCount)
if err != nil {
cli.Log.Errorf("Failed to get prekeys to upload: %v", err)
return
}
cli.Log.Infof("Uploading %d new prekeys to server", len(preKeys))
_, err = cli.sendIQ(infoQuery{
Namespace: "encrypt",
Type: "set",
To: types.ServerJID,
Content: []waBinary.Node{
{Tag: "registration", Content: registrationIDBytes[:]},
{Tag: "type", Content: []byte{ecc.DjbType}},
{Tag: "identity", Content: cli.Store.IdentityKey.Pub[:]},
{Tag: "list", Content: preKeysToNodes(preKeys)},
preKeyToNode(cli.Store.SignedPreKey),
},
})
if err != nil {
cli.Log.Errorf("Failed to send request to upload prekeys: %v", err)
return
}
cli.Log.Debugf("Got response to uploading prekeys")
err = cli.Store.PreKeys.MarkPreKeysAsUploaded(preKeys[len(preKeys)-1].KeyID)
if err != nil {
cli.Log.Warnf("Failed to mark prekeys as uploaded: %v", err)
}
cli.lastPreKeyUpload = time.Now()
}
type preKeyResp struct {
bundle *prekey.Bundle
err error
}
func (cli *Client) fetchPreKeys(users []types.JID) (map[types.JID]preKeyResp, error) {
requests := make([]waBinary.Node, len(users))
for i, user := range users {
requests[i].Tag = "user"
requests[i].Attrs = waBinary.Attrs{
"jid": user,
"reason": "identity",
}
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "encrypt",
Type: "get",
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "key",
Content: requests,
}},
})
if err != nil {
return nil, fmt.Errorf("failed to send prekey request: %w", err)
} else if len(resp.GetChildren()) == 0 {
return nil, fmt.Errorf("got empty response to prekey request")
}
list := resp.GetChildByTag("list")
respData := make(map[types.JID]preKeyResp)
for _, child := range list.GetChildren() {
if child.Tag != "user" {
continue
}
jid := child.AttrGetter().JID("jid")
jid.AD = true
bundle, err := nodeToPreKeyBundle(uint32(jid.Device), child)
respData[jid] = preKeyResp{bundle, err}
}
return respData, nil
}
func preKeyToNode(key *keys.PreKey) waBinary.Node {
var keyID [4]byte
binary.BigEndian.PutUint32(keyID[:], key.KeyID)
node := waBinary.Node{
Tag: "key",
Content: []waBinary.Node{
{Tag: "id", Content: keyID[1:]},
{Tag: "value", Content: key.Pub[:]},
},
}
if key.Signature != nil {
node.Tag = "skey"
node.Content = append(node.GetChildren(), waBinary.Node{
Tag: "signature",
Content: key.Signature[:],
})
}
return node
}
func nodeToPreKeyBundle(deviceID uint32, node waBinary.Node) (*prekey.Bundle, error) {
errorNode, ok := node.GetOptionalChildByTag("error")
if ok && errorNode.Tag == "error" {
return nil, fmt.Errorf("got error getting prekeys: %s", errorNode.XMLString())
}
registrationBytes, ok := node.GetChildByTag("registration").Content.([]byte)
if !ok || len(registrationBytes) != 4 {
return nil, fmt.Errorf("invalid registration ID in prekey response")
}
registrationID := binary.BigEndian.Uint32(registrationBytes)
keysNode, ok := node.GetOptionalChildByTag("keys")
if !ok {
keysNode = node
}
identityKeyRaw, ok := keysNode.GetChildByTag("identity").Content.([]byte)
if !ok || len(identityKeyRaw) != 32 {
return nil, fmt.Errorf("invalid identity key in prekey response")
}
identityKeyPub := *(*[32]byte)(identityKeyRaw)
preKey, err := nodeToPreKey(keysNode.GetChildByTag("key"))
if err != nil {
return nil, fmt.Errorf("invalid prekey in prekey response: %w", err)
}
signedPreKey, err := nodeToPreKey(keysNode.GetChildByTag("skey"))
if err != nil {
return nil, fmt.Errorf("invalid signed prekey in prekey response: %w", err)
}
return prekey.NewBundle(registrationID, deviceID,
optional.NewOptionalUint32(preKey.KeyID), signedPreKey.KeyID,
ecc.NewDjbECPublicKey(*preKey.Pub), ecc.NewDjbECPublicKey(*signedPreKey.Pub), *signedPreKey.Signature,
identity.NewKey(ecc.NewDjbECPublicKey(identityKeyPub))), nil
}
func nodeToPreKey(node waBinary.Node) (*keys.PreKey, error) {
key := keys.PreKey{
KeyPair: keys.KeyPair{},
KeyID: 0,
Signature: nil,
}
if id := node.GetChildByTag("id"); id.Tag != "id" {
return nil, fmt.Errorf("prekey node doesn't contain ID tag")
} else if idBytes, ok := id.Content.([]byte); !ok {
return nil, fmt.Errorf("prekey ID has unexpected content (%T)", id.Content)
} else if len(idBytes) != 3 {
return nil, fmt.Errorf("prekey ID has unexpected number of bytes (%d, expected 3)", len(idBytes))
} else {
key.KeyID = binary.BigEndian.Uint32(append([]byte{0}, idBytes...))
}
if pubkey := node.GetChildByTag("value"); pubkey.Tag != "value" {
return nil, fmt.Errorf("prekey node doesn't contain value tag")
} else if pubkeyBytes, ok := pubkey.Content.([]byte); !ok {
return nil, fmt.Errorf("prekey value has unexpected content (%T)", pubkey.Content)
} else if len(pubkeyBytes) != 32 {
return nil, fmt.Errorf("prekey value has unexpected number of bytes (%d, expected 32)", len(pubkeyBytes))
} else {
key.KeyPair.Pub = (*[32]byte)(pubkeyBytes)
}
if node.Tag == "skey" {
if sig := node.GetChildByTag("signature"); sig.Tag != "signature" {
return nil, fmt.Errorf("prekey node doesn't contain signature tag")
} else if sigBytes, ok := sig.Content.([]byte); !ok {
return nil, fmt.Errorf("prekey signature has unexpected content (%T)", sig.Content)
} else if len(sigBytes) != 64 {
return nil, fmt.Errorf("prekey signature has unexpected number of bytes (%d, expected 64)", len(sigBytes))
} else {
key.Signature = (*[64]byte)(sigBytes)
}
}
return &key, nil
}
func preKeysToNodes(prekeys []*keys.PreKey) []waBinary.Node {
nodes := make([]waBinary.Node, len(prekeys))
for i, key := range prekeys {
nodes[i] = preKeyToNode(key)
}
return nodes
}

101
vendor/go.mau.fi/whatsmeow/presence.go vendored Normal file
View File

@ -0,0 +1,101 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
func (cli *Client) handleChatState(node *waBinary.Node) {
source, err := cli.parseMessageSource(node)
if err != nil {
cli.Log.Warnf("Failed to parse chat state update: %v", err)
} else if len(node.GetChildren()) != 1 {
cli.Log.Warnf("Failed to parse chat state update: unexpected number of children in element (%d)", len(node.GetChildren()))
} else {
child := node.GetChildren()[0]
presence := types.ChatPresence(child.Tag)
if presence != types.ChatPresenceComposing && presence != types.ChatPresenceRecording && presence != types.ChatPresencePaused {
cli.Log.Warnf("Unrecognized chat presence state %s", child.Tag)
}
cli.dispatchEvent(&events.ChatPresence{
MessageSource: source,
State: presence,
})
}
}
func (cli *Client) handlePresence(node *waBinary.Node) {
var evt events.Presence
ag := node.AttrGetter()
evt.From = ag.JID("from")
presenceType := ag.OptionalString("type")
if presenceType == "unavailable" {
evt.Unavailable = true
} else if presenceType != "" {
cli.Log.Debugf("Unrecognized presence type '%s' in presence event from %s", presenceType, evt.From)
}
lastSeen := ag.OptionalString("last")
if lastSeen != "" && lastSeen != "deny" {
evt.LastSeen = time.Unix(ag.Int64("last"), 0)
}
if !ag.OK() {
cli.Log.Warnf("Error parsing presence event: %+v", ag.Errors)
} else {
cli.dispatchEvent(&evt)
}
}
// SendPresence updates the user's presence status on WhatsApp.
//
// You should call this at least once after connecting so that the server has your pushname.
// Otherwise, other users will see "-" as the name.
func (cli *Client) SendPresence(state types.Presence) error {
if len(cli.Store.PushName) == 0 {
return ErrNoPushName
}
return cli.sendNode(waBinary.Node{
Tag: "presence",
Attrs: waBinary.Attrs{
"name": cli.Store.PushName,
"type": string(state),
},
})
}
// SubscribePresence asks the WhatsApp servers to send presence updates of a specific user to this client.
//
// After subscribing to this event, you should start receiving *events.Presence for that user in normal event handlers.
//
// Also, it seems that the WhatsApp servers require you to be online to receive presence status from other users,
// so you should mark yourself as online before trying to use this function:
// cli.SendPresence(types.PresenceAvailable)
func (cli *Client) SubscribePresence(jid types.JID) error {
return cli.sendNode(waBinary.Node{
Tag: "presence",
Attrs: waBinary.Attrs{
"type": "subscribe",
"to": jid,
},
})
}
// SendChatPresence updates the user's typing status in a specific chat.
func (cli *Client) SendChatPresence(state types.ChatPresence, jid types.JID) error {
return cli.sendNode(waBinary.Node{
Tag: "chatstate",
Attrs: waBinary.Attrs{
"from": *cli.Store.ID,
"to": jid,
},
Content: []waBinary.Node{{Tag: string(state)}},
})
}

View File

@ -0,0 +1,93 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
// TryFetchPrivacySettings will fetch the user's privacy settings, either from the in-memory cache or from the server.
func (cli *Client) TryFetchPrivacySettings(ignoreCache bool) (*types.PrivacySettings, error) {
if val := cli.privacySettingsCache.Load(); val != nil && !ignoreCache {
return val.(*types.PrivacySettings), nil
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "privacy",
Type: iqGet,
To: types.ServerJID,
Content: []waBinary.Node{{Tag: "privacy"}},
})
if err != nil {
return nil, err
}
privacyNode, ok := resp.GetOptionalChildByTag("privacy")
if !ok {
return nil, &ElementMissingError{Tag: "privacy", In: "response to privacy settings query"}
}
var settings types.PrivacySettings
cli.parsePrivacySettings(&privacyNode, &settings)
cli.privacySettingsCache.Store(&settings)
return &settings, nil
}
// GetPrivacySettings will get the user's privacy settings. If an error occurs while fetching them, the error will be
// logged, but the method will just return an empty struct.
func (cli *Client) GetPrivacySettings() (settings types.PrivacySettings) {
settingsPtr, err := cli.TryFetchPrivacySettings(false)
if err != nil {
cli.Log.Errorf("Failed to fetch privacy settings: %v", err)
} else {
settings = *settingsPtr
}
return
}
func (cli *Client) parsePrivacySettings(privacyNode *waBinary.Node, settings *types.PrivacySettings) *events.PrivacySettings {
var evt events.PrivacySettings
for _, child := range privacyNode.GetChildren() {
if child.Tag != "category" {
continue
}
ag := child.AttrGetter()
name := ag.String("name")
value := types.PrivacySetting(ag.String("value"))
switch name {
case "groupadd":
settings.GroupAdd = value
evt.GroupAddChanged = true
case "last":
settings.LastSeen = value
evt.LastSeenChanged = true
case "status":
settings.Status = value
evt.StatusChanged = true
case "profile":
settings.Profile = value
evt.ProfileChanged = true
case "readreceipts":
settings.ReadReceipts = value
evt.ReadReceiptsChanged = true
}
}
return &evt
}
func (cli *Client) handlePrivacySettingsNotification(privacyNode *waBinary.Node) {
cli.Log.Debugf("Parsing privacy settings change notification")
settings, err := cli.TryFetchPrivacySettings(false)
if err != nil {
cli.Log.Errorf("Failed to fetch privacy settings when handling change: %v", err)
}
evt := cli.parsePrivacySettings(privacyNode, settings)
// The data isn't be reliable if the fetch failed, so only cache if it didn't fail
if err == nil {
cli.privacySettingsCache.Store(settings)
}
cli.dispatchEvent(evt)
}

168
vendor/go.mau.fi/whatsmeow/qrchan.go vendored Normal file
View File

@ -0,0 +1,168 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"sync"
"sync/atomic"
"time"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
)
type QRChannelItem struct {
// The type of event, "code" for new QR codes.
// 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.
Error error
// If the item is a new code, then this field contains the raw data.
Code string
// The timeout after which the next code will be sent down the channel.
Timeout time.Duration
}
var (
// QRChannelSuccess is emitted from GetQRChannel when the pairing is successful.
QRChannelSuccess = QRChannelItem{Event: "success"}
// QRChannelTimeout is emitted from GetQRChannel if the socket gets disconnected by the server before the pairing is successful.
QRChannelTimeout = QRChannelItem{Event: "timeout"}
// QRChannelErrUnexpectedEvent is emitted from GetQRChannel if an unexpected connection event is received,
// as that likely means that the pairing has already happened before the channel was set up.
QRChannelErrUnexpectedEvent = QRChannelItem{Event: "err-unexpected-state"}
// QRChannelScannedWithoutMultidevice is emitted from GetQRChannel if events.QRScannedWithoutMultidevice is received.
QRChannelScannedWithoutMultidevice = QRChannelItem{Event: "err-scanned-without-multidevice"}
)
type qrChannel struct {
sync.Mutex
cli *Client
log waLog.Logger
ctx context.Context
handlerID uint32
closed uint32
output chan<- QRChannelItem
stopQRs chan struct{}
}
func (qrc *qrChannel) emitQRs(evt *events.QR) {
var nextCode string
for {
if len(evt.Codes) == 0 {
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
qrc.log.Debugf("Ran out of QR codes, closing channel with status %s and disconnecting client", QRChannelTimeout)
qrc.output <- QRChannelTimeout
close(qrc.output)
go qrc.cli.RemoveEventHandler(qrc.handlerID)
qrc.cli.Disconnect()
} else {
qrc.log.Debugf("Ran out of QR codes, but channel is already closed")
}
return
} else if atomic.LoadUint32(&qrc.closed) == 1 {
qrc.log.Debugf("QR code channel is closed, exiting QR emitter")
return
}
timeout := 20 * time.Second
if len(evt.Codes) == 6 {
timeout = 60 * time.Second
}
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"}:
default:
qrc.log.Debugf("Output channel didn't accept code, exiting QR emitter")
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
close(qrc.output)
go qrc.cli.RemoveEventHandler(qrc.handlerID)
qrc.cli.Disconnect()
}
return
}
select {
case <-time.After(timeout):
case <-qrc.stopQRs:
qrc.log.Debugf("Got signal to stop QR emitter")
return
case <-qrc.ctx.Done():
qrc.log.Debugf("Context is done, stopping QR emitter")
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
close(qrc.output)
go qrc.cli.RemoveEventHandler(qrc.handlerID)
qrc.cli.Disconnect()
}
}
}
}
func (qrc *qrChannel) handleEvent(rawEvt interface{}) {
if atomic.LoadUint32(&qrc.closed) == 1 {
qrc.log.Debugf("Dropping event of type %T, channel is closed", rawEvt)
return
}
var outputType QRChannelItem
switch evt := rawEvt.(type) {
case *events.QR:
qrc.log.Debugf("Received QR code event, starting to emit codes to channel")
go qrc.emitQRs(evt)
return
case *events.QRScannedWithoutMultidevice:
qrc.log.Debugf("QR code scanned without multidevice enabled")
qrc.output <- QRChannelScannedWithoutMultidevice
return
case *events.PairSuccess:
outputType = QRChannelSuccess
case *events.PairError:
outputType = QRChannelItem{
Event: "error",
Error: evt.Error,
}
case *events.Disconnected:
outputType = QRChannelTimeout
case *events.Connected, *events.ConnectFailure, *events.LoggedOut:
outputType = QRChannelErrUnexpectedEvent
default:
return
}
close(qrc.stopQRs)
if atomic.CompareAndSwapUint32(&qrc.closed, 0, 1) {
qrc.log.Debugf("Closing channel with status %+v", outputType)
qrc.output <- outputType
close(qrc.output)
} else {
qrc.log.Debugf("Got status %+v, but channel is already closed", outputType)
}
// Has to be done in background because otherwise there's a deadlock with eventHandlersLock
go qrc.cli.RemoveEventHandler(qrc.handlerID)
}
// GetQRChannel returns a channel that automatically outputs a new QR code when the previous one expires.
//
// 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",
// 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() {
return nil, ErrQRAlreadyConnected
} else if cli.Store.ID != nil {
return nil, ErrQRStoreContainsID
}
ch := make(chan QRChannelItem, 8)
qrc := qrChannel{
output: ch,
stopQRs: make(chan struct{}),
cli: cli,
log: cli.Log.Sub("QRChannel"),
ctx: ctx,
}
qrc.handlerID = cli.AddEventHandler(qrc.handleEvent)
return ch, nil
}

148
vendor/go.mau.fi/whatsmeow/receipt.go vendored Normal file
View File

@ -0,0 +1,148 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"fmt"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
func (cli *Client) handleReceipt(node *waBinary.Node) {
receipt, err := cli.parseReceipt(node)
if err != nil {
cli.Log.Warnf("Failed to parse receipt: %v", err)
} else {
if receipt.Type == events.ReceiptTypeRetry {
go func() {
err := cli.handleRetryReceipt(receipt, node)
if err != nil {
cli.Log.Errorf("Failed to handle retry receipt for %s/%s from %s: %v", receipt.Chat, receipt.MessageIDs[0], receipt.Sender, err)
}
}()
}
go cli.dispatchEvent(receipt)
}
go cli.sendAck(node)
}
func (cli *Client) parseReceipt(node *waBinary.Node) (*events.Receipt, error) {
ag := node.AttrGetter()
source, err := cli.parseMessageSource(node)
if err != nil {
return nil, err
}
receipt := events.Receipt{
MessageSource: source,
Timestamp: time.Unix(ag.Int64("t"), 0),
Type: events.ReceiptType(ag.OptionalString("type")),
}
mainMessageID := ag.String("id")
if !ag.OK() {
return nil, fmt.Errorf("failed to parse read receipt attrs: %+v", ag.Errors)
}
receiptChildren := node.GetChildren()
if len(receiptChildren) == 1 && receiptChildren[0].Tag == "list" {
listChildren := receiptChildren[0].GetChildren()
receipt.MessageIDs = make([]string, 1, len(listChildren)+1)
receipt.MessageIDs[0] = mainMessageID
for _, item := range listChildren {
if id, ok := item.Attrs["id"].(string); ok && item.Tag == "item" {
receipt.MessageIDs = append(receipt.MessageIDs, id)
}
}
} else {
receipt.MessageIDs = []types.MessageID{mainMessageID}
}
return &receipt, nil
}
func (cli *Client) sendAck(node *waBinary.Node) {
attrs := waBinary.Attrs{
"class": node.Tag,
"id": node.Attrs["id"],
}
attrs["to"] = node.Attrs["from"]
if participant, ok := node.Attrs["participant"]; ok {
attrs["participant"] = participant
}
if recipient, ok := node.Attrs["recipient"]; ok {
attrs["recipient"] = recipient
}
if receiptType, ok := node.Attrs["type"]; node.Tag != "message" && ok {
attrs["type"] = receiptType
}
err := cli.sendNode(waBinary.Node{
Tag: "ack",
Attrs: attrs,
})
if err != nil {
cli.Log.Warnf("Failed to send acknowledgement for %s %s: %v", node.Tag, node.Attrs["id"], err)
}
}
// MarkRead sends a read receipt for the given message IDs including the given timestamp as the read at time.
//
// The first JID parameter (chat) must always be set to the chat ID (user ID in DMs and group ID in group chats).
// The second JID parameter (sender) must be set in group chats and must be the user ID who sent the message.
func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, sender types.JID) error {
node := waBinary.Node{
Tag: "receipt",
Attrs: waBinary.Attrs{
"id": ids[0],
"type": "read",
"to": chat,
"t": timestamp.Unix(),
},
}
if cli.GetPrivacySettings().ReadReceipts == types.PrivacySettingNone {
node.Attrs["type"] = "read-self"
}
if !sender.IsEmpty() && chat.Server != types.DefaultUserServer {
node.Attrs["participant"] = sender.ToNonAD()
}
if len(ids) > 1 {
children := make([]waBinary.Node, len(ids)-1)
for i := 1; i < len(ids); i++ {
children[i-1].Tag = "item"
children[i-1].Attrs = waBinary.Attrs{"id": ids[i]}
}
node.Content = []waBinary.Node{{
Tag: "list",
Content: children,
}}
}
return cli.sendNode(node)
}
func (cli *Client) sendMessageReceipt(info *types.MessageInfo) {
attrs := waBinary.Attrs{
"id": info.ID,
}
if info.IsFromMe {
attrs["type"] = "sender"
} else {
attrs["type"] = "inactive"
}
attrs["to"] = info.Chat
if info.IsGroup {
attrs["participant"] = info.Sender
} else if info.IsFromMe {
attrs["recipient"] = info.Sender
}
err := cli.sendNode(waBinary.Node{
Tag: "receipt",
Attrs: attrs,
})
if err != nil {
cli.Log.Warnf("Failed to send receipt for %s: %v", info.ID, err)
}
}

141
vendor/go.mau.fi/whatsmeow/request.go vendored Normal file
View File

@ -0,0 +1,141 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"strconv"
"sync/atomic"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
func (cli *Client) generateRequestID() string {
return cli.uniqueID + strconv.FormatUint(uint64(atomic.AddUint32(&cli.idCounter, 1)), 10)
}
var closedNode = &waBinary.Node{Tag: "xmlstreamend"}
func (cli *Client) clearResponseWaiters() {
cli.responseWaitersLock.Lock()
for _, waiter := range cli.responseWaiters {
select {
case waiter <- closedNode:
default:
close(waiter)
}
}
cli.responseWaiters = make(map[string]chan<- *waBinary.Node)
cli.responseWaitersLock.Unlock()
}
func (cli *Client) waitResponse(reqID string) chan *waBinary.Node {
ch := make(chan *waBinary.Node, 1)
cli.responseWaitersLock.Lock()
cli.responseWaiters[reqID] = ch
cli.responseWaitersLock.Unlock()
return ch
}
func (cli *Client) cancelResponse(reqID string, ch chan *waBinary.Node) {
cli.responseWaitersLock.Lock()
close(ch)
delete(cli.responseWaiters, reqID)
cli.responseWaitersLock.Unlock()
}
func (cli *Client) receiveResponse(data *waBinary.Node) bool {
id, ok := data.Attrs["id"].(string)
if !ok || (data.Tag != "iq" && data.Tag != "ack") {
return false
}
cli.responseWaitersLock.Lock()
waiter, ok := cli.responseWaiters[id]
if !ok {
cli.responseWaitersLock.Unlock()
return false
}
delete(cli.responseWaiters, id)
cli.responseWaitersLock.Unlock()
waiter <- data
return true
}
type infoQueryType string
const (
iqSet infoQueryType = "set"
iqGet infoQueryType = "get"
)
type infoQuery struct {
Namespace string
Type infoQueryType
To types.JID
ID string
Content interface{}
Timeout time.Duration
Context context.Context
}
func (cli *Client) sendIQAsync(query infoQuery) (<-chan *waBinary.Node, error) {
if len(query.ID) == 0 {
query.ID = cli.generateRequestID()
}
waiter := cli.waitResponse(query.ID)
attrs := waBinary.Attrs{
"id": query.ID,
"xmlns": query.Namespace,
"type": string(query.Type),
}
if !query.To.IsEmpty() {
attrs["to"] = query.To
}
err := cli.sendNode(waBinary.Node{
Tag: "iq",
Attrs: attrs,
Content: query.Content,
})
if err != nil {
cli.cancelResponse(query.ID, waiter)
return nil, err
}
return waiter, nil
}
func (cli *Client) sendIQ(query infoQuery) (*waBinary.Node, error) {
resChan, err := cli.sendIQAsync(query)
if err != nil {
return nil, err
}
if query.Timeout == 0 {
query.Timeout = 1 * time.Minute
}
if query.Context == nil {
query.Context = context.Background()
}
select {
case res := <-resChan:
if res == closedNode {
return nil, ErrIQDisconnected
}
resType, _ := res.Attrs["type"].(string)
if res.Tag != "iq" || (resType != "result" && resType != "error") {
return res, &IQError{RawNode: res}
} else if resType == "error" {
return res, parseIQError(res)
}
return res, nil
case <-query.Context.Done():
return nil, query.Context.Err()
case <-time.After(query.Timeout):
return nil, ErrIQTimedOut
}
}

264
vendor/go.mau.fi/whatsmeow/retry.go vendored Normal file
View File

@ -0,0 +1,264 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"encoding/binary"
"fmt"
"time"
"go.mau.fi/libsignal/ecc"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/keys/prekey"
"go.mau.fi/libsignal/protocol"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
// Number of sent messages to cache in memory for handling retry receipts.
const recentMessagesSize = 256
type recentMessageKey struct {
To types.JID
ID types.MessageID
}
// RecentMessage contains the info needed to re-send a message when another device fails to decrypt it.
type RecentMessage struct {
Proto *waProto.Message
Timestamp time.Time
}
func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, message *waProto.Message) {
cli.recentMessagesLock.Lock()
key := recentMessageKey{to, id}
if cli.recentMessagesList[cli.recentMessagesPtr].ID != "" {
delete(cli.recentMessagesMap, cli.recentMessagesList[cli.recentMessagesPtr])
}
cli.recentMessagesMap[key] = message
cli.recentMessagesList[cli.recentMessagesPtr] = key
cli.recentMessagesPtr++
if cli.recentMessagesPtr >= len(cli.recentMessagesList) {
cli.recentMessagesPtr = 0
}
cli.recentMessagesLock.Unlock()
}
func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) *waProto.Message {
cli.recentMessagesLock.RLock()
msg, _ := cli.recentMessagesMap[recentMessageKey{to, id}]
cli.recentMessagesLock.RUnlock()
return msg
}
func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.MessageID) (*waProto.Message, error) {
msg := cli.getRecentMessage(receipt.Chat, messageID)
if msg == nil {
msg = cli.GetMessageForRetry(receipt.Chat, messageID)
if msg == nil {
return nil, fmt.Errorf("couldn't find message %s", messageID)
} else {
cli.Log.Debugf("Found message in GetMessageForRetry to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
}
} else {
cli.Log.Debugf("Found message in local cache to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
}
return proto.Clone(msg).(*waProto.Message), nil
}
// handleRetryReceipt handles an incoming retry receipt for an outgoing message.
func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.Node) error {
retryChild, ok := node.GetOptionalChildByTag("retry")
if !ok {
return &ElementMissingError{Tag: "retry", In: "retry receipt"}
}
ag := retryChild.AttrGetter()
messageID := ag.String("id")
timestamp := time.Unix(ag.Int64("t"), 0)
retryCount := ag.Int("count")
if !ag.OK() {
return ag.Error()
}
msg, err := cli.getMessageForRetry(receipt, messageID)
if err != nil {
return err
}
if receipt.IsGroup {
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), cli.Store.ID.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)
} else {
msg.SenderKeyDistributionMessage = &waProto.SenderKeyDistributionMessage{
GroupId: proto.String(receipt.Chat.String()),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
}
}
} else if receipt.IsFromMe {
msg = &waProto.Message{
DeviceSentMessage: &waProto.DeviceSentMessage{
DestinationJid: proto.String(receipt.Chat.String()),
Message: msg,
},
}
}
if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, retryCount, msg) {
cli.Log.Debugf("Cancelled retry receipt in PreRetryCallback")
return nil
}
plaintext, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
_, hasKeys := node.GetOptionalChildByTag("keys")
var bundle *prekey.Bundle
if hasKeys {
bundle, err = nodeToPreKeyBundle(uint32(receipt.Sender.Device), *node)
if err != nil {
return fmt.Errorf("failed to read prekey bundle in retry receipt: %w", err)
}
} else if retryCount >= 2 || !cli.Store.ContainsSession(receipt.Sender.SignalAddress()) {
if retryCount >= 2 {
cli.Log.Debugf("Fetching prekeys for %s due to retry receipt with count>1 but no prekey bundle", receipt.Sender)
} else {
cli.Log.Debugf("Fetching prekeys for %s for handling retry receipt because we don't have a Signal session with them", receipt.Sender)
}
var keys map[types.JID]preKeyResp
keys, err = cli.fetchPreKeys([]types.JID{receipt.Sender})
if err != nil {
return err
}
senderAD := receipt.Sender
senderAD.AD = true
bundle, err = keys[senderAD].bundle, keys[senderAD].err
if err != nil {
return fmt.Errorf("failed to fetch prekeys: %w", err)
} else if bundle == nil {
return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", senderAD, len(keys))
}
}
encrypted, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle)
if err != nil {
return fmt.Errorf("failed to encrypt message for retry: %w", err)
}
encrypted.Attrs["count"] = retryCount
attrs := waBinary.Attrs{
"to": node.Attrs["from"],
"type": "text",
"id": messageID,
"t": timestamp.Unix(),
}
if participant, ok := node.Attrs["participant"]; ok {
attrs["participant"] = participant
}
if recipient, ok := node.Attrs["recipient"]; ok {
attrs["recipient"] = recipient
}
if edit, ok := node.Attrs["edit"]; ok {
attrs["edit"] = edit
}
req := waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: []waBinary.Node{*encrypted},
}
if includeDeviceIdentity {
err = cli.appendDeviceIdentityNode(&req)
if err != nil {
return fmt.Errorf("failed to add device identity to retry message: %w", err)
}
}
err = cli.sendNode(req)
if err != nil {
return fmt.Errorf("failed to send retry message: %w", err)
}
cli.Log.Debugf("Sent retry #%d for %s/%s to %s", retryCount, receipt.Chat, messageID, receipt.Sender)
return nil
}
// sendRetryReceipt sends a retry receipt for an incoming message.
func (cli *Client) sendRetryReceipt(node *waBinary.Node, forceIncludeIdentity bool) {
id, _ := node.Attrs["id"].(string)
children := node.GetChildren()
var retryCountInMsg int
if len(children) == 1 && children[0].Tag == "enc" {
retryCountInMsg = children[0].AttrGetter().OptionalInt("count")
}
cli.messageRetriesLock.Lock()
cli.messageRetries[id]++
retryCount := cli.messageRetries[id]
// In case the message is a retry response, and we restarted in between, find the count from the message
if retryCount == 1 && retryCountInMsg > 0 {
retryCount = retryCountInMsg + 1
cli.messageRetries[id] = retryCount
}
cli.messageRetriesLock.Unlock()
if retryCount >= 5 {
cli.Log.Warnf("Not sending any more retry receipts for %s", id)
return
}
var registrationIDBytes [4]byte
binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID)
attrs := waBinary.Attrs{
"id": id,
"type": "retry",
"to": node.Attrs["from"],
}
if recipient, ok := node.Attrs["recipient"]; ok {
attrs["recipient"] = recipient
}
if participant, ok := node.Attrs["participant"]; ok {
attrs["participant"] = participant
}
payload := waBinary.Node{
Tag: "receipt",
Attrs: attrs,
Content: []waBinary.Node{
{Tag: "retry", Attrs: waBinary.Attrs{
"count": retryCount,
"id": id,
"t": node.Attrs["t"],
"v": 1,
}},
{Tag: "registration", Content: registrationIDBytes[:]},
},
}
if retryCount > 1 || forceIncludeIdentity {
if key, err := cli.Store.PreKeys.GenOnePreKey(); err != nil {
cli.Log.Errorf("Failed to get prekey for retry receipt: %v", err)
} else if deviceIdentity, err := proto.Marshal(cli.Store.Account); err != nil {
cli.Log.Errorf("Failed to marshal account info: %v", err)
return
} else {
payload.Content = append(payload.GetChildren(), waBinary.Node{
Tag: "keys",
Content: []waBinary.Node{
{Tag: "type", Content: []byte{ecc.DjbType}},
{Tag: "identity", Content: cli.Store.IdentityKey.Pub[:]},
preKeyToNode(key),
preKeyToNode(cli.Store.SignedPreKey),
{Tag: "device-identity", Content: deviceIdentity},
},
})
}
}
err := cli.sendNode(payload)
if err != nil {
cli.Log.Errorf("Failed to send retry receipt for %s: %v", id, err)
}
}

357
vendor/go.mau.fi/whatsmeow/send.go vendored Normal file
View File

@ -0,0 +1,357 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"sort"
"strings"
"time"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/keys/prekey"
"go.mau.fi/libsignal/protocol"
"go.mau.fi/libsignal/session"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
)
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
func GenerateMessageID() types.MessageID {
id := make([]byte, 16)
_, err := rand.Read(id)
if err != nil {
// Out of entropy
panic(err)
}
return strings.ToUpper(hex.EncodeToString(id))
}
// SendMessage sends the given message.
//
// If the message ID is not provided, a random message ID will be generated.
//
// 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.
func (cli *Client) SendMessage(to types.JID, id types.MessageID, message *waProto.Message) (time.Time, error) {
if to.AD {
return time.Time{}, ErrRecipientADJID
}
if len(id) == 0 {
id = GenerateMessageID()
}
cli.addRecentMessage(to, id, message)
respChan := cli.waitResponse(id)
var err error
var phash string
switch to.Server {
case types.GroupServer:
phash, err = cli.sendGroup(to, id, message)
case types.DefaultUserServer:
err = cli.sendDM(to, id, message)
case types.BroadcastServer:
err = ErrBroadcastListUnsupported
default:
err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server)
}
if err != nil {
cli.cancelResponse(id, respChan)
return time.Time{}, err
}
resp := <-respChan
if resp == closedNode {
return time.Time{}, ErrSendDisconnected
}
ag := resp.AttrGetter()
ts := time.Unix(ag.Int64("t"), 0)
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)
// TODO also invalidate device list caches
cli.groupParticipantsCacheLock.Lock()
delete(cli.groupParticipantsCache, to)
cli.groupParticipantsCacheLock.Unlock()
}
return ts, nil
}
// RevokeMessage deletes the given message from everyone in the chat.
// You can only revoke your own messages, and if the message is too old, then other users will ignore the deletion.
//
// This method will wait for the server to acknowledge the revocation message before returning.
// The return value is the timestamp of the message from the server.
func (cli *Client) RevokeMessage(chat types.JID, id types.MessageID) (time.Time, error) {
return cli.SendMessage(chat, cli.generateRequestID(), &waProto.Message{
ProtocolMessage: &waProto.ProtocolMessage{
Type: waProto.ProtocolMessage_REVOKE.Enum(),
Key: &waProto.MessageKey{
FromMe: proto.Bool(true),
Id: proto.String(id),
RemoteJid: proto.String(chat.String()),
},
},
})
}
func participantListHashV2(participants []types.JID) string {
participantsStrings := make([]string, len(participants))
for i, part := range participants {
participantsStrings[i] = part.String()
}
sort.Strings(participantsStrings)
hash := sha256.Sum256([]byte(strings.Join(participantsStrings, "")))
return fmt.Sprintf("2:%s", base64.RawStdEncoding.EncodeToString(hash[:6]))
}
func (cli *Client) sendGroup(to types.JID, id types.MessageID, message *waProto.Message) (string, error) {
participants, err := cli.getGroupMembers(to)
if err != nil {
return "", fmt.Errorf("failed to get group members: %w", err)
}
plaintext, _, err := marshalMessage(to, message)
if err != nil {
return "", err
}
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(to.String(), cli.Store.ID.SignalAddress())
signalSKDMessage, err := builder.Create(senderKeyName)
if err != nil {
return "", fmt.Errorf("failed to create sender key distribution message to send %s to %s: %w", id, to, err)
}
skdMessage := &waProto.Message{
SenderKeyDistributionMessage: &waProto.SenderKeyDistributionMessage{
GroupId: proto.String(to.String()),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
},
}
skdPlaintext, err := proto.Marshal(skdMessage)
if err != nil {
return "", fmt.Errorf("failed to marshal sender key distribution message to send %s to %s: %w", id, to, err)
}
cipher := groups.NewGroupCipher(builder, senderKeyName, cli.Store)
encrypted, err := cipher.Encrypt(padMessage(plaintext))
if err != nil {
return "", fmt.Errorf("failed to encrypt group message to send %s to %s: %w", id, to, err)
}
ciphertext := encrypted.SignedSerialize()
node, allDevices, err := cli.prepareMessageNode(to, id, message, participants, skdPlaintext, nil)
if err != nil {
return "", err
}
phash := participantListHashV2(allDevices)
node.Attrs["phash"] = phash
node.Content = append(node.GetChildren(), waBinary.Node{
Tag: "enc",
Content: ciphertext,
Attrs: waBinary.Attrs{"v": "2", "type": "skmsg"},
})
err = cli.sendNode(*node)
if err != nil {
return "", fmt.Errorf("failed to send message node: %w", err)
}
return phash, nil
}
func (cli *Client) sendDM(to types.JID, id types.MessageID, message *waProto.Message) error {
messagePlaintext, deviceSentMessagePlaintext, err := marshalMessage(to, message)
if err != nil {
return err
}
node, _, err := cli.prepareMessageNode(to, id, message, []types.JID{to, *cli.Store.ID}, messagePlaintext, deviceSentMessagePlaintext)
if err != nil {
return err
}
err = cli.sendNode(*node)
if err != nil {
return fmt.Errorf("failed to send message node: %w", err)
}
return nil
}
func (cli *Client) prepareMessageNode(to types.JID, id types.MessageID, message *waProto.Message, participants []types.JID, plaintext, dsmPlaintext []byte) (*waBinary.Node, []types.JID, error) {
allDevices, err := cli.GetUserDevices(participants)
if err != nil {
return nil, nil, fmt.Errorf("failed to get device list: %w", err)
}
participantNodes, includeIdentity := cli.encryptMessageForDevices(allDevices, id, plaintext, dsmPlaintext)
node := waBinary.Node{
Tag: "message",
Attrs: waBinary.Attrs{
"id": id,
"type": "text",
"to": to,
},
Content: []waBinary.Node{{
Tag: "participants",
Content: participantNodes,
}},
}
if message.ProtocolMessage != nil && message.GetProtocolMessage().GetType() == waProto.ProtocolMessage_REVOKE {
node.Attrs["edit"] = "7"
}
if includeIdentity {
err := cli.appendDeviceIdentityNode(&node)
if err != nil {
return nil, nil, err
}
}
return &node, allDevices, nil
}
func marshalMessage(to types.JID, message *waProto.Message) (plaintext, dsmPlaintext []byte, err error) {
plaintext, err = proto.Marshal(message)
if err != nil {
err = fmt.Errorf("failed to marshal message: %w", err)
return
}
if to.Server != types.GroupServer {
dsmPlaintext, err = proto.Marshal(&waProto.Message{
DeviceSentMessage: &waProto.DeviceSentMessage{
DestinationJid: proto.String(to.String()),
Message: message,
},
})
if err != nil {
err = fmt.Errorf("failed to marshal message (for own devices): %w", err)
return
}
}
return
}
func (cli *Client) appendDeviceIdentityNode(node *waBinary.Node) error {
deviceIdentity, err := proto.Marshal(cli.Store.Account)
if err != nil {
return fmt.Errorf("failed to marshal device identity: %w", err)
}
node.Content = append(node.GetChildren(), waBinary.Node{
Tag: "device-identity",
Content: deviceIdentity,
})
return nil
}
func (cli *Client) encryptMessageForDevices(allDevices []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 {
continue
}
plaintext = dsmPlaintext
}
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, nil)
if errors.Is(err, ErrNoSession) {
retryDevices = append(retryDevices, jid)
continue
} else if err != nil {
cli.Log.Warnf("Failed to encrypt %s for %s: %v", id, jid, err)
continue
}
participantNodes = append(participantNodes, *encrypted)
if isPreKey {
includeIdentity = true
}
}
if len(retryDevices) > 0 {
bundles, err := cli.fetchPreKeys(retryDevices)
if err != nil {
cli.Log.Warnf("Failed to fetch prekeys for %d to retry encryption: %v", retryDevices, err)
} else {
for _, jid := range retryDevices {
resp := bundles[jid]
if resp.err != nil {
cli.Log.Warnf("Failed to fetch prekey for %s: %v", jid, resp.err)
continue
}
plaintext := msgPlaintext
if jid.User == cli.Store.ID.User && dsmPlaintext != nil {
plaintext = dsmPlaintext
}
encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap(plaintext, jid, resp.bundle)
if err != nil {
cli.Log.Warnf("Failed to encrypt %s for %s (retry): %v", id, jid, err)
continue
}
participantNodes = append(participantNodes, *encrypted)
if isPreKey {
includeIdentity = true
}
}
}
}
return participantNodes, includeIdentity
}
func (cli *Client) encryptMessageForDeviceAndWrap(plaintext []byte, to types.JID, bundle *prekey.Bundle) (*waBinary.Node, bool, error) {
node, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, to, bundle)
if err != nil {
return nil, false, err
}
return &waBinary.Node{
Tag: "to",
Attrs: waBinary.Attrs{"jid": to},
Content: []waBinary.Node{*node},
}, includeDeviceIdentity, nil
}
func (cli *Client) encryptMessageForDevice(plaintext []byte, to types.JID, bundle *prekey.Bundle) (*waBinary.Node, bool, error) {
builder := session.NewBuilderFromSignal(cli.Store, to.SignalAddress(), pbSerializer)
if bundle != nil {
cli.Log.Debugf("Processing prekey bundle for %s", to)
err := builder.ProcessBundle(bundle)
if err != nil {
return nil, false, fmt.Errorf("failed to process prekey bundle: %w", err)
}
} else if !cli.Store.ContainsSession(to.SignalAddress()) {
return nil, false, ErrNoSession
}
cipher := session.NewCipher(builder, to.SignalAddress())
ciphertext, err := cipher.Encrypt(padMessage(plaintext))
if err != nil {
return nil, false, fmt.Errorf("cipher encryption failed: %w", err)
}
encType := "msg"
if ciphertext.Type() == protocol.PREKEY_TYPE {
encType = "pkmsg"
}
return &waBinary.Node{
Tag: "enc",
Attrs: waBinary.Attrs{
"v": "2",
"type": encType,
},
Content: ciphertext.Serialize(),
}, encType == "pkmsg", nil
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package socket implements a subset of the Noise protocol framework on top of websockets as used by WhatsApp.
//
// There shouldn't be any need to manually interact with this package.
// The Client struct in the top-level whatsmeow package handles everything.
package socket
import "errors"
const (
// Origin is the Origin header for all WhatsApp websocket connections
Origin = "https://web.whatsapp.com"
// URL is the websocket URL for the new multidevice protocol
URL = "wss://web.whatsapp.com/ws/chat"
)
const (
NoiseStartPattern = "Noise_XX_25519_AESGCM_SHA256\x00\x00\x00\x00"
WADictVersion = 2
WAMagicValue = 5
)
var WAConnHeader = []byte{'W', 'A', WAMagicValue, WADictVersion}
const (
FrameMaxSize = 2 << 23
FrameLengthSize = 3
)
var (
ErrFrameTooLarge = errors.New("frame too large")
ErrSocketClosed = errors.New("frame socket is closed")
ErrSocketAlreadyOpen = errors.New("frame socket is already open")
)

View File

@ -0,0 +1,228 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package socket
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
waLog "go.mau.fi/whatsmeow/util/log"
)
type FrameSocket struct {
conn *websocket.Conn
ctx context.Context
cancel func()
log waLog.Logger
lock sync.Mutex
Frames chan []byte
OnDisconnect func(remote bool)
WriteTimeout time.Duration
Header []byte
incomingLength int
receivedLength int
incoming []byte
partialHeader []byte
}
func NewFrameSocket(log waLog.Logger, header []byte) *FrameSocket {
return &FrameSocket{
conn: nil,
log: log,
Header: header,
Frames: make(chan []byte),
}
}
func (fs *FrameSocket) IsConnected() bool {
return fs.conn != nil
}
func (fs *FrameSocket) Context() context.Context {
return fs.ctx
}
func (fs *FrameSocket) Close(code int) {
fs.lock.Lock()
defer fs.lock.Unlock()
if fs.conn == nil {
return
}
if code > 0 {
message := websocket.FormatCloseMessage(code, "")
err := fs.conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
if err != nil {
fs.log.Warnf("Error sending close message: %v", err)
}
}
fs.cancel()
err := fs.conn.Close()
if err != nil {
fs.log.Errorf("Error closing websocket: %v", err)
}
fs.conn = nil
fs.ctx = nil
fs.cancel = nil
if fs.OnDisconnect != nil {
go fs.OnDisconnect(code == 0)
}
}
func (fs *FrameSocket) Connect() error {
fs.lock.Lock()
defer fs.lock.Unlock()
if fs.conn != nil {
return ErrSocketAlreadyOpen
}
ctx, cancel := context.WithCancel(context.Background())
dialer := websocket.Dialer{}
headers := http.Header{"Origin": []string{Origin}}
fs.log.Debugf("Dialing %s", URL)
conn, _, err := dialer.Dial(URL, headers)
if err != nil {
cancel()
return fmt.Errorf("couldn't dial whatsapp web websocket: %w", err)
}
fs.ctx, fs.cancel = ctx, cancel
fs.conn = conn
conn.SetCloseHandler(func(code int, text string) error {
fs.log.Debugf("Server closed websocket with status %d/%s", code, text)
cancel()
// from default CloseHandler
message := websocket.FormatCloseMessage(code, "")
_ = conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
return nil
})
go fs.readPump(conn, ctx)
return nil
}
func (fs *FrameSocket) SendFrame(data []byte) error {
conn := fs.conn
if conn == nil {
return ErrSocketClosed
}
dataLength := len(data)
if dataLength >= FrameMaxSize {
return fmt.Errorf("%w (got %d bytes, max %d bytes)", ErrFrameTooLarge, len(data), FrameMaxSize)
}
headerLength := len(fs.Header)
// Whole frame is header + 3 bytes for length + data
wholeFrame := make([]byte, headerLength+FrameLengthSize+dataLength)
// Copy the header if it's there
if fs.Header != nil {
copy(wholeFrame[:headerLength], fs.Header)
// We only want to send the header once
fs.Header = nil
}
// Encode length of frame
wholeFrame[headerLength] = byte(dataLength >> 16)
wholeFrame[headerLength+1] = byte(dataLength >> 8)
wholeFrame[headerLength+2] = byte(dataLength)
// Copy actual frame data
copy(wholeFrame[headerLength+FrameLengthSize:], data)
if fs.WriteTimeout > 0 {
err := conn.SetWriteDeadline(time.Now().Add(fs.WriteTimeout))
if err != nil {
fs.log.Warnf("Failed to set write deadline: %v", err)
}
}
return conn.WriteMessage(websocket.BinaryMessage, wholeFrame)
}
func (fs *FrameSocket) frameComplete() {
data := fs.incoming
fs.incoming = nil
fs.partialHeader = nil
fs.incomingLength = 0
fs.receivedLength = 0
fs.Frames <- data
}
func (fs *FrameSocket) processData(msg []byte) {
for len(msg) > 0 {
// This probably doesn't happen a lot (if at all), so the code is unoptimized
if fs.partialHeader != nil {
msg = append(fs.partialHeader, msg...)
fs.partialHeader = nil
}
if fs.incoming == nil {
if len(msg) >= FrameLengthSize {
length := (int(msg[0]) << 16) + (int(msg[1]) << 8) + int(msg[2])
fs.incomingLength = length
fs.receivedLength = len(msg)
msg = msg[FrameLengthSize:]
if len(msg) >= length {
fs.incoming = msg[:length]
msg = msg[length:]
fs.frameComplete()
} else {
fs.incoming = make([]byte, length)
copy(fs.incoming, msg)
msg = nil
}
} else {
fs.log.Warnf("Received partial header (report if this happens often)")
fs.partialHeader = msg
msg = nil
}
} else {
if len(fs.incoming)+len(msg) >= fs.incomingLength {
copy(fs.incoming[fs.receivedLength:], msg[:fs.incomingLength-fs.receivedLength])
msg = msg[fs.incomingLength-fs.receivedLength:]
fs.frameComplete()
} else {
copy(fs.incoming[fs.receivedLength:], msg)
fs.receivedLength += len(msg)
msg = nil
}
}
}
}
func (fs *FrameSocket) readPump(conn *websocket.Conn, ctx context.Context) {
fs.log.Debugf("Frame websocket read pump starting %p", fs)
defer func() {
fs.log.Debugf("Frame websocket read pump exiting %p", fs)
go fs.Close(0)
}()
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
// Ignore the error if the context has been closed
if !errors.Is(ctx.Err(), context.Canceled) {
fs.log.Errorf("Error reading from websocket: %v", err)
}
return
} else if msgType != websocket.BinaryMessage {
fs.log.Warnf("Got unexpected websocket message type %d", msgType)
continue
}
fs.processData(data)
}
}

View File

@ -0,0 +1,135 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package socket
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"fmt"
"io"
"sync/atomic"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
)
type NoiseHandshake struct {
hash []byte
salt []byte
key cipher.AEAD
counter uint32
}
func NewNoiseHandshake() *NoiseHandshake {
return &NoiseHandshake{}
}
func newCipher(key []byte) (cipher.AEAD, error) {
aesCipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCM(aesCipher)
if err != nil {
return nil, err
}
return aesGCM, nil
}
func sha256Slice(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
}
func (nh *NoiseHandshake) Start(pattern string, header []byte) {
data := []byte(pattern)
if len(data) == 32 {
nh.hash = data
} else {
nh.hash = sha256Slice(data)
}
nh.salt = nh.hash
var err error
nh.key, err = newCipher(nh.hash)
if err != nil {
panic(err)
}
nh.Authenticate(header)
}
func (nh *NoiseHandshake) Authenticate(data []byte) {
nh.hash = sha256Slice(append(nh.hash, data...))
}
func (nh *NoiseHandshake) postIncrementCounter() uint32 {
count := atomic.AddUint32(&nh.counter, 1)
return count - 1
}
func (nh *NoiseHandshake) Encrypt(plaintext []byte) []byte {
ciphertext := nh.key.Seal(nil, generateIV(nh.postIncrementCounter()), plaintext, nh.hash)
nh.Authenticate(ciphertext)
return ciphertext
}
func (nh *NoiseHandshake) Decrypt(ciphertext []byte) (plaintext []byte, err error) {
plaintext, err = nh.key.Open(nil, generateIV(nh.postIncrementCounter()), ciphertext, nh.hash)
if err == nil {
nh.Authenticate(ciphertext)
}
return
}
func (nh *NoiseHandshake) Finish(fs *FrameSocket, frameHandler FrameHandler, disconnectHandler DisconnectHandler) (*NoiseSocket, error) {
if write, read, err := nh.extractAndExpand(nh.salt, nil); err != nil {
return nil, fmt.Errorf("failed to extract final keys: %w", err)
} else if writeKey, err := newCipher(write); err != nil {
return nil, fmt.Errorf("failed to create final write cipher: %w", err)
} else if readKey, err := newCipher(read); err != nil {
return nil, fmt.Errorf("failed to create final read cipher: %w", err)
} else if ns, err := newNoiseSocket(fs, writeKey, readKey, frameHandler, disconnectHandler); err != nil {
return nil, fmt.Errorf("failed to create noise socket: %w", err)
} else {
return ns, nil
}
}
func (nh *NoiseHandshake) MixSharedSecretIntoKey(priv, pub [32]byte) error {
secret, err := curve25519.X25519(priv[:], pub[:])
if err != nil {
return fmt.Errorf("failed to do x25519 scalar multiplication: %w", err)
}
return nh.MixIntoKey(secret)
}
func (nh *NoiseHandshake) MixIntoKey(data []byte) error {
nh.counter = 0
write, read, err := nh.extractAndExpand(nh.salt, data)
if err != nil {
return fmt.Errorf("failed to extract keys for mixing: %w", err)
}
nh.salt = write
nh.key, err = newCipher(read)
if err != nil {
return fmt.Errorf("failed to create new cipher while mixing keys: %w", err)
}
return nil
}
func (nh *NoiseHandshake) extractAndExpand(salt, data []byte) (write []byte, read []byte, err error) {
h := hkdf.New(sha256.New, data, salt, nil)
write = make([]byte, 32)
read = make([]byte, 32)
if _, err = io.ReadFull(h, write); err != nil {
err = fmt.Errorf("failed to read write key: %w", err)
} else if _, err = io.ReadFull(h, read); err != nil {
err = fmt.Errorf("failed to read read key: %w", err)
}
return
}

View File

@ -0,0 +1,104 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package socket
import (
"context"
"crypto/cipher"
"encoding/binary"
"sync"
"sync/atomic"
"github.com/gorilla/websocket"
)
type NoiseSocket struct {
fs *FrameSocket
onFrame FrameHandler
writeKey cipher.AEAD
readKey cipher.AEAD
writeCounter uint32
readCounter uint32
writeLock sync.Mutex
destroyed uint32
stopConsumer chan struct{}
}
type DisconnectHandler func(socket *NoiseSocket, remote bool)
type FrameHandler func([]byte)
func newNoiseSocket(fs *FrameSocket, writeKey, readKey cipher.AEAD, frameHandler FrameHandler, disconnectHandler DisconnectHandler) (*NoiseSocket, error) {
ns := &NoiseSocket{
fs: fs,
writeKey: writeKey,
readKey: readKey,
onFrame: frameHandler,
stopConsumer: make(chan struct{}),
}
fs.OnDisconnect = func(remote bool) {
disconnectHandler(ns, remote)
}
go ns.consumeFrames(fs.ctx, fs.Frames)
return ns, nil
}
func (ns *NoiseSocket) consumeFrames(ctx context.Context, frames <-chan []byte) {
ctxDone := ctx.Done()
for {
select {
case frame := <-frames:
ns.receiveEncryptedFrame(frame)
case <-ctxDone:
return
case <-ns.stopConsumer:
return
}
}
}
func generateIV(count uint32) []byte {
iv := make([]byte, 12)
binary.BigEndian.PutUint32(iv[8:], count)
return iv
}
func (ns *NoiseSocket) Context() context.Context {
return ns.fs.Context()
}
func (ns *NoiseSocket) Stop(disconnect bool) {
if atomic.CompareAndSwapUint32(&ns.destroyed, 0, 1) {
close(ns.stopConsumer)
ns.fs.OnDisconnect = nil
if disconnect {
ns.fs.Close(websocket.CloseNormalClosure)
}
}
}
func (ns *NoiseSocket) SendFrame(plaintext []byte) error {
ns.writeLock.Lock()
ciphertext := ns.writeKey.Seal(nil, generateIV(ns.writeCounter), plaintext, nil)
ns.writeCounter++
err := ns.fs.SendFrame(ciphertext)
ns.writeLock.Unlock()
return err
}
func (ns *NoiseSocket) receiveEncryptedFrame(ciphertext []byte) {
count := atomic.AddUint32(&ns.readCounter, 1) - 1
plaintext, err := ns.readKey.Open(nil, generateIV(count), ciphertext, nil)
if err != nil {
ns.fs.log.Warnf("Failed to decrypt frame: %v", err)
return
}
ns.onFrame(plaintext)
}
func (ns *NoiseSocket) IsConnected() bool {
return ns.fs.IsConnected()
}

View File

@ -0,0 +1,118 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package store
import (
"crypto/md5"
"encoding/binary"
"fmt"
"strconv"
"strings"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/ecc"
waProto "go.mau.fi/whatsmeow/binary/proto"
)
// waVersion is the WhatsApp web client version
var waVersion = []uint32{2, 2202, 9}
// waVersionHash is the md5 hash of a dot-separated waVersion
var waVersionHash [16]byte
func init() {
waVersionParts := make([]string, len(waVersion))
for i, part := range waVersion {
waVersionParts[i] = strconv.Itoa(int(part))
}
waVersionString := strings.Join(waVersionParts, ".")
waVersionHash = md5.Sum([]byte(waVersionString))
}
var BaseClientPayload = &waProto.ClientPayload{
UserAgent: &waProto.UserAgent{
Platform: waProto.UserAgent_WEB.Enum(),
ReleaseChannel: waProto.UserAgent_RELEASE.Enum(),
AppVersion: &waProto.AppVersion{
Primary: &waVersion[0],
Secondary: &waVersion[1],
Tertiary: &waVersion[2],
},
Mcc: proto.String("000"),
Mnc: proto.String("000"),
OsVersion: proto.String("0.1.0"),
Manufacturer: proto.String(""),
Device: proto.String("Desktop"),
OsBuildNumber: proto.String("0.1.0"),
LocaleLanguageIso6391: proto.String("en"),
LocaleCountryIso31661Alpha2: proto.String("en"),
},
WebInfo: &waProto.WebInfo{
WebSubPlatform: waProto.WebInfo_WEB_BROWSER.Enum(),
},
ConnectType: waProto.ClientPayload_WIFI_UNKNOWN.Enum(),
ConnectReason: waProto.ClientPayload_USER_ACTIVATED.Enum(),
}
var CompanionProps = &waProto.CompanionProps{
Os: proto.String("whatsmeow"),
Version: &waProto.AppVersion{
Primary: proto.Uint32(0),
Secondary: proto.Uint32(1),
Tertiary: proto.Uint32(0),
},
PlatformType: waProto.CompanionProps_UNKNOWN.Enum(),
RequireFullSync: proto.Bool(false),
}
func SetOSInfo(name string, version [3]uint32) {
CompanionProps.Os = &name
CompanionProps.Version.Primary = &version[0]
CompanionProps.Version.Secondary = &version[1]
CompanionProps.Version.Tertiary = &version[2]
BaseClientPayload.UserAgent.OsVersion = proto.String(fmt.Sprintf("%d.%d.%d", version[0], version[1], version[2]))
BaseClientPayload.UserAgent.OsBuildNumber = BaseClientPayload.UserAgent.OsVersion
}
func (device *Device) getRegistrationPayload() *waProto.ClientPayload {
payload := proto.Clone(BaseClientPayload).(*waProto.ClientPayload)
regID := make([]byte, 4)
binary.BigEndian.PutUint32(regID, device.RegistrationID)
preKeyID := make([]byte, 4)
binary.BigEndian.PutUint32(preKeyID, device.SignedPreKey.KeyID)
companionProps, _ := proto.Marshal(CompanionProps)
payload.RegData = &waProto.CompanionRegData{
ERegid: regID,
EKeytype: []byte{ecc.DjbType},
EIdent: device.IdentityKey.Pub[:],
ESkeyId: preKeyID[1:],
ESkeyVal: device.SignedPreKey.Pub[:],
ESkeySig: device.SignedPreKey.Signature[:],
BuildHash: waVersionHash[:],
CompanionProps: companionProps,
}
payload.Passive = proto.Bool(false)
return payload
}
func (device *Device) getLoginPayload() *waProto.ClientPayload {
payload := proto.Clone(BaseClientPayload).(*waProto.ClientPayload)
payload.Username = proto.Uint64(device.ID.UserInt())
payload.Device = proto.Uint32(uint32(device.ID.Device))
payload.Passive = proto.Bool(true)
return payload
}
func (device *Device) GetClientPayload() *waProto.ClientPayload {
if device.ID != nil {
return device.getLoginPayload()
} else {
return device.getRegistrationPayload()
}
}

View File

@ -0,0 +1,168 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package store
import (
"go.mau.fi/libsignal/ecc"
groupRecord "go.mau.fi/libsignal/groups/state/record"
"go.mau.fi/libsignal/keys/identity"
"go.mau.fi/libsignal/protocol"
"go.mau.fi/libsignal/serialize"
"go.mau.fi/libsignal/state/record"
"go.mau.fi/libsignal/state/store"
)
var SignalProtobufSerializer = serialize.NewProtoBufSerializer()
var _ store.SignalProtocol = (*Device)(nil)
func (device *Device) GetIdentityKeyPair() *identity.KeyPair {
return identity.NewKeyPair(
identity.NewKey(ecc.NewDjbECPublicKey(*device.IdentityKey.Pub)),
ecc.NewDjbECPrivateKey(*device.IdentityKey.Priv),
)
}
func (device *Device) GetLocalRegistrationId() uint32 {
return device.RegistrationID
}
func (device *Device) SaveIdentity(address *protocol.SignalAddress, identityKey *identity.Key) {
err := device.Identities.PutIdentity(address.String(), identityKey.PublicKey().PublicKey())
if err != nil {
device.Log.Errorf("Failed to save identity of %s: %v", address.String(), err)
}
}
func (device *Device) IsTrustedIdentity(address *protocol.SignalAddress, identityKey *identity.Key) bool {
isTrusted, err := device.Identities.IsTrustedIdentity(address.String(), identityKey.PublicKey().PublicKey())
if err != nil {
device.Log.Errorf("Failed to check if %s's identity is trusted: %v", address.String(), err)
}
return isTrusted
}
func (device *Device) LoadPreKey(id uint32) *record.PreKey {
preKey, err := device.PreKeys.GetPreKey(id)
if err != nil {
device.Log.Errorf("Failed to load prekey %d: %v", id, err)
return nil
} else if preKey == nil {
return nil
}
return record.NewPreKey(preKey.KeyID, ecc.NewECKeyPair(
ecc.NewDjbECPublicKey(*preKey.Pub),
ecc.NewDjbECPrivateKey(*preKey.Priv),
), nil)
}
func (device *Device) RemovePreKey(id uint32) {
err := device.PreKeys.RemovePreKey(id)
if err != nil {
device.Log.Errorf("Failed to remove prekey %d: %v", id, err)
}
}
func (device *Device) StorePreKey(preKeyID uint32, preKeyRecord *record.PreKey) {
panic("not implemented")
}
func (device *Device) ContainsPreKey(preKeyID uint32) bool {
panic("not implemented")
}
func (device *Device) LoadSession(address *protocol.SignalAddress) *record.Session {
rawSess, err := device.Sessions.GetSession(address.String())
if err != nil {
device.Log.Errorf("Failed to load session with %s: %v", address.String(), err)
return record.NewSession(SignalProtobufSerializer.Session, SignalProtobufSerializer.State)
} else if rawSess == nil {
return record.NewSession(SignalProtobufSerializer.Session, SignalProtobufSerializer.State)
}
sess, err := record.NewSessionFromBytes(rawSess, SignalProtobufSerializer.Session, SignalProtobufSerializer.State)
if err != nil {
device.Log.Errorf("Failed to deserialize session with %s: %v", address.String(), err)
return record.NewSession(SignalProtobufSerializer.Session, SignalProtobufSerializer.State)
}
return sess
}
func (device *Device) GetSubDeviceSessions(name string) []uint32 {
panic("not implemented")
}
func (device *Device) StoreSession(address *protocol.SignalAddress, record *record.Session) {
err := device.Sessions.PutSession(address.String(), record.Serialize())
if err != nil {
device.Log.Errorf("Failed to store session with %s: %v", address.String(), err)
}
}
func (device *Device) ContainsSession(remoteAddress *protocol.SignalAddress) bool {
hasSession, err := device.Sessions.HasSession(remoteAddress.String())
if err != nil {
device.Log.Warnf("Failed to check if store has session for %s: %v", remoteAddress.String(), err)
}
return hasSession
}
func (device *Device) DeleteSession(remoteAddress *protocol.SignalAddress) {
panic("not implemented")
}
func (device *Device) DeleteAllSessions() {
panic("not implemented")
}
func (device *Device) LoadSignedPreKey(signedPreKeyID uint32) *record.SignedPreKey {
if signedPreKeyID == device.SignedPreKey.KeyID {
return record.NewSignedPreKey(signedPreKeyID, 0, ecc.NewECKeyPair(
ecc.NewDjbECPublicKey(*device.SignedPreKey.Pub),
ecc.NewDjbECPrivateKey(*device.SignedPreKey.Priv),
), *device.SignedPreKey.Signature, nil)
}
return nil
}
func (device *Device) LoadSignedPreKeys() []*record.SignedPreKey {
panic("not implemented")
}
func (device *Device) StoreSignedPreKey(signedPreKeyID uint32, record *record.SignedPreKey) {
panic("not implemented")
}
func (device *Device) ContainsSignedPreKey(signedPreKeyID uint32) bool {
panic("not implemented")
}
func (device *Device) RemoveSignedPreKey(signedPreKeyID uint32) {
panic("not implemented")
}
func (device *Device) StoreSenderKey(senderKeyName *protocol.SenderKeyName, keyRecord *groupRecord.SenderKey) {
err := device.SenderKeys.PutSenderKey(senderKeyName.GroupID(), senderKeyName.Sender().String(), keyRecord.Serialize())
if err != nil {
device.Log.Errorf("Failed to store sender key from %s for %s: %v", senderKeyName.Sender().String(), senderKeyName.GroupID(), err)
}
}
func (device *Device) LoadSenderKey(senderKeyName *protocol.SenderKeyName) *groupRecord.SenderKey {
rawKey, err := device.SenderKeys.GetSenderKey(senderKeyName.GroupID(), senderKeyName.Sender().String())
if err != nil {
device.Log.Errorf("Failed to load sender key from %s for %s: %v", senderKeyName.Sender().String(), senderKeyName.GroupID(), err)
return groupRecord.NewSenderKey(SignalProtobufSerializer.SenderKeyRecord, SignalProtobufSerializer.SenderKeyState)
} else if rawKey == nil {
return groupRecord.NewSenderKey(SignalProtobufSerializer.SenderKeyRecord, SignalProtobufSerializer.SenderKeyState)
}
key, err := groupRecord.NewSenderKeyFromBytes(rawKey, SignalProtobufSerializer.SenderKeyRecord, SignalProtobufSerializer.SenderKeyState)
if err != nil {
device.Log.Errorf("Failed to deserialize sender key from %s for %s: %v", senderKeyName.Sender().String(), senderKeyName.GroupID(), err)
return groupRecord.NewSenderKey(SignalProtobufSerializer.SenderKeyRecord, SignalProtobufSerializer.SenderKeyState)
}
return key
}

View File

@ -0,0 +1,245 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package sqlstore
import (
"crypto/rand"
"database/sql"
"errors"
"fmt"
mathRand "math/rand"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
)
// Container is a wrapper for a SQL database that can contain multiple whatsmeow sessions.
type Container struct {
db *sql.DB
dialect string
log waLog.Logger
}
var _ store.DeviceContainer = (*Container)(nil)
// New connects to the given SQL database and wraps it in a Container.
//
// Only SQLite and Postgres are currently fully supported.
//
// The logger can be nil and will default to a no-op logger.
//
// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`:
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
func New(dialect, address string, log waLog.Logger) (*Container, error) {
db, err := sql.Open(dialect, address)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
container := NewWithDB(db, dialect, log)
err = container.Upgrade()
if err != nil {
return nil, fmt.Errorf("failed to upgrade database: %w", err)
}
return container, nil
}
// NewWithDB wraps an existing SQL connection in a Container.
//
// Only SQLite and Postgres are currently fully supported.
//
// The logger can be nil and will default to a no-op logger.
//
// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`:
// db, err := sql.Open("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on")
// if err != nil {
// panic(err)
// }
// container, err := sqlstore.NewWithDB(db, "sqlite3", nil)
func NewWithDB(db *sql.DB, dialect string, log waLog.Logger) *Container {
if log == nil {
log = waLog.Noop
}
return &Container{
db: db,
dialect: dialect,
log: log,
}
}
const getAllDevicesQuery = `
SELECT jid, registration_id, noise_key, identity_key,
signed_pre_key, signed_pre_key_id, signed_pre_key_sig,
adv_key, adv_details, adv_account_sig, adv_device_sig,
platform, business_name, push_name
FROM whatsmeow_device
`
const getDeviceQuery = getAllDevicesQuery + " WHERE jid=$1"
type scannable interface {
Scan(dest ...interface{}) error
}
func (c *Container) scanDevice(row scannable) (*store.Device, error) {
var device store.Device
device.Log = c.log
device.SignedPreKey = &keys.PreKey{}
var noisePriv, identityPriv, preKeyPriv, preKeySig []byte
var account waProto.ADVSignedDeviceIdentity
err := row.Scan(
&device.ID, &device.RegistrationID, &noisePriv, &identityPriv,
&preKeyPriv, &device.SignedPreKey.KeyID, &preKeySig,
&device.AdvSecretKey, &account.Details, &account.AccountSignature, &account.DeviceSignature,
&device.Platform, &device.BusinessName, &device.PushName)
if err != nil {
return nil, fmt.Errorf("failed to scan session: %w", err)
} else if len(noisePriv) != 32 || len(identityPriv) != 32 || len(preKeyPriv) != 32 || len(preKeySig) != 64 {
return nil, ErrInvalidLength
}
device.NoiseKey = keys.NewKeyPairFromPrivateKey(*(*[32]byte)(noisePriv))
device.IdentityKey = keys.NewKeyPairFromPrivateKey(*(*[32]byte)(identityPriv))
device.SignedPreKey.KeyPair = *keys.NewKeyPairFromPrivateKey(*(*[32]byte)(preKeyPriv))
device.SignedPreKey.Signature = (*[64]byte)(preKeySig)
device.Account = &account
innerStore := NewSQLStore(c, *device.ID)
device.Identities = innerStore
device.Sessions = innerStore
device.PreKeys = innerStore
device.SenderKeys = innerStore
device.AppStateKeys = innerStore
device.AppState = innerStore
device.Contacts = innerStore
device.ChatSettings = innerStore
device.Container = c
device.Initialized = true
return &device, nil
}
// GetAllDevices finds all the devices in the database.
func (c *Container) GetAllDevices() ([]*store.Device, error) {
res, err := c.db.Query(getAllDevicesQuery)
if err != nil {
return nil, fmt.Errorf("failed to query sessions: %w", err)
}
sessions := make([]*store.Device, 0)
for res.Next() {
sess, scanErr := c.scanDevice(res)
if scanErr != nil {
return sessions, scanErr
}
sessions = append(sessions, sess)
}
return sessions, nil
}
// GetFirstDevice is a convenience method for getting the first device in the store. If there are
// no devices, then a new device will be created. You should only use this if you don't want to
// have multiple sessions simultaneously.
func (c *Container) GetFirstDevice() (*store.Device, error) {
devices, err := c.GetAllDevices()
if err != nil {
return nil, err
}
if len(devices) == 0 {
return c.NewDevice(), nil
} else {
return devices[0], nil
}
}
// GetDevice finds the device with the specified JID in the database.
//
// If the device is not found, nil is returned instead.
//
// Note that the parameter usually must be an AD-JID.
func (c *Container) GetDevice(jid types.JID) (*store.Device, error) {
sess, err := c.scanDevice(c.db.QueryRow(getDeviceQuery, jid))
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return sess, err
}
const (
insertDeviceQuery = `
INSERT INTO whatsmeow_device (jid, registration_id, noise_key, identity_key,
signed_pre_key, signed_pre_key_id, signed_pre_key_sig,
adv_key, adv_details, adv_account_sig, adv_device_sig,
platform, business_name, push_name)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
ON CONFLICT (jid) DO UPDATE SET platform=$12, business_name=$13, push_name=$14
`
deleteDeviceQuery = `DELETE FROM whatsmeow_device WHERE jid=$1`
)
// NewDevice creates a new device in this database.
//
// No data is actually stored before Save is called. However, the pairing process will automatically
// call Save after a successful pairing, so you most likely don't need to call it yourself.
func (c *Container) NewDevice() *store.Device {
device := &store.Device{
Log: c.log,
Container: c,
NoiseKey: keys.NewKeyPair(),
IdentityKey: keys.NewKeyPair(),
RegistrationID: mathRand.Uint32(),
AdvSecretKey: make([]byte, 32),
}
_, err := rand.Read(device.AdvSecretKey)
if err != nil {
panic(err)
}
device.SignedPreKey = device.IdentityKey.CreateSignedPreKey(1)
return device
}
// ErrDeviceIDMustBeSet is the error returned by PutDevice if you try to save a device before knowing its JID.
var ErrDeviceIDMustBeSet = errors.New("device JID must be known before accessing database")
// PutDevice stores the given device in this database. This should be called through Device.Save()
// (which usually doesn't need to be called manually, as the library does that automatically when relevant).
func (c *Container) PutDevice(device *store.Device) error {
if device.ID == nil {
return ErrDeviceIDMustBeSet
}
_, err := c.db.Exec(insertDeviceQuery,
device.ID.String(), device.RegistrationID, device.NoiseKey.Priv[:], device.IdentityKey.Priv[:],
device.SignedPreKey.Priv[:], device.SignedPreKey.KeyID, device.SignedPreKey.Signature[:],
device.AdvSecretKey, device.Account.Details, device.Account.AccountSignature, device.Account.DeviceSignature,
device.Platform, device.BusinessName, device.PushName)
if !device.Initialized {
innerStore := NewSQLStore(c, *device.ID)
device.Identities = innerStore
device.Sessions = innerStore
device.PreKeys = innerStore
device.SenderKeys = innerStore
device.AppStateKeys = innerStore
device.AppState = innerStore
device.Contacts = innerStore
device.ChatSettings = innerStore
device.Initialized = true
}
return err
}
// DeleteDevice deletes the given device from this database. This should be called through Device.Delete()
func (c *Container) DeleteDevice(store *store.Device) error {
if store.ID == nil {
return ErrDeviceIDMustBeSet
}
_, err := c.db.Exec(deleteDeviceQuery, store.ID.String())
return err
}

View File

@ -0,0 +1,610 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package sqlstore contains an SQL-backed implementation of the interfaces in the store package.
package sqlstore
import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"strings"
"sync"
"time"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
)
// ErrInvalidLength is returned by some database getters if the database returned a byte array with an unexpected length.
// This should be impossible, as the database schema contains CHECK()s for all the relevant columns.
var ErrInvalidLength = errors.New("database returned byte array with illegal length")
// PostgresArrayWrapper is a function to wrap array values before passing them to the sql package.
//
// When using github.com/lib/pq, you should set
// whatsmeow.PostgresArrayWrapper = pq.Array
var PostgresArrayWrapper func(interface{}) interface {
driver.Valuer
sql.Scanner
}
type SQLStore struct {
*Container
JID string
preKeyLock sync.Mutex
contactCache map[types.JID]*types.ContactInfo
contactCacheLock sync.Mutex
}
// NewSQLStore creates a new SQLStore with the given database container and user JID.
// It contains implementations of all the different stores in the store package.
//
// In general, you should use Container.NewDevice or Container.GetDevice instead of this.
func NewSQLStore(c *Container, jid types.JID) *SQLStore {
return &SQLStore{
Container: c,
JID: jid.String(),
contactCache: make(map[types.JID]*types.ContactInfo),
}
}
var _ store.IdentityStore = (*SQLStore)(nil)
var _ store.SessionStore = (*SQLStore)(nil)
var _ store.PreKeyStore = (*SQLStore)(nil)
var _ store.SenderKeyStore = (*SQLStore)(nil)
var _ store.AppStateSyncKeyStore = (*SQLStore)(nil)
var _ store.AppStateStore = (*SQLStore)(nil)
var _ store.ContactStore = (*SQLStore)(nil)
const (
putIdentityQuery = `
INSERT INTO whatsmeow_identity_keys (our_jid, their_id, identity) VALUES ($1, $2, $3)
ON CONFLICT (our_jid, their_id) DO UPDATE SET identity=$3
`
deleteAllIdentitiesQuery = `DELETE FROM whatsmeow_identity_keys WHERE our_jid=$1 AND their_id LIKE $2`
deleteIdentityQuery = `DELETE FROM whatsmeow_identity_keys WHERE our_jid=$1 AND their_id=$2`
getIdentityQuery = `SELECT identity FROM whatsmeow_identity_keys WHERE our_jid=$1 AND their_id=$2`
)
func (s *SQLStore) PutIdentity(address string, key [32]byte) error {
_, err := s.db.Exec(putIdentityQuery, s.JID, address, key[:])
return err
}
func (s *SQLStore) DeleteAllIdentities(phone string) error {
_, err := s.db.Exec(deleteAllIdentitiesQuery, s.JID, phone+":%")
return err
}
func (s *SQLStore) DeleteIdentity(address string) error {
_, err := s.db.Exec(deleteAllIdentitiesQuery, s.JID, address)
return err
}
func (s *SQLStore) IsTrustedIdentity(address string, key [32]byte) (bool, error) {
var existingIdentity []byte
err := s.db.QueryRow(getIdentityQuery, s.JID, address).Scan(&existingIdentity)
if errors.Is(err, sql.ErrNoRows) {
// Trust if not known, it'll be saved automatically later
return true, nil
} else if err != nil {
return false, err
} else if len(existingIdentity) != 32 {
return false, ErrInvalidLength
}
return *(*[32]byte)(existingIdentity) == key, nil
}
const (
getSessionQuery = `SELECT session FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2`
hasSessionQuery = `SELECT true FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2`
putSessionQuery = `
INSERT INTO whatsmeow_sessions (our_jid, their_id, session) VALUES ($1, $2, $3)
ON CONFLICT (our_jid, their_id) DO UPDATE SET session=$3
`
deleteAllSessionsQuery = `DELETE FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id LIKE $2`
deleteSessionQuery = `DELETE FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2`
)
func (s *SQLStore) GetSession(address string) (session []byte, err error) {
err = s.db.QueryRow(getSessionQuery, s.JID, address).Scan(&session)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *SQLStore) HasSession(address string) (has bool, err error) {
err = s.db.QueryRow(hasSessionQuery, s.JID, address).Scan(&has)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (s *SQLStore) PutSession(address string, session []byte) error {
_, err := s.db.Exec(putSessionQuery, s.JID, address, session)
return err
}
func (s *SQLStore) DeleteAllSessions(phone string) error {
_, err := s.db.Exec(deleteAllSessionsQuery, s.JID, phone+":%")
return err
}
func (s *SQLStore) DeleteSession(address string) error {
_, err := s.db.Exec(deleteSessionQuery, s.JID, address)
return err
}
const (
getLastPreKeyIDQuery = `SELECT MAX(key_id) FROM whatsmeow_pre_keys WHERE jid=$1`
insertPreKeyQuery = `INSERT INTO whatsmeow_pre_keys (jid, key_id, key, uploaded) VALUES ($1, $2, $3, $4)`
getUnuploadedPreKeysQuery = `SELECT key_id, key FROM whatsmeow_pre_keys WHERE jid=$1 AND uploaded=false ORDER BY key_id LIMIT $2`
getPreKeyQuery = `SELECT key_id, key FROM whatsmeow_pre_keys WHERE jid=$1 AND key_id=$2`
deletePreKeyQuery = `DELETE FROM whatsmeow_pre_keys WHERE jid=$1 AND key_id=$2`
markPreKeysAsUploadedQuery = `UPDATE whatsmeow_pre_keys SET uploaded=true WHERE jid=$1 AND key_id<=$2`
getUploadedPreKeyCountQuery = `SELECT COUNT(*) FROM whatsmeow_pre_keys WHERE jid=$1 AND uploaded=true`
)
func (s *SQLStore) genOnePreKey(id uint32, markUploaded bool) (*keys.PreKey, error) {
key := keys.NewPreKey(id)
_, err := s.db.Exec(insertPreKeyQuery, s.JID, key.KeyID, key.Priv[:], markUploaded)
return key, err
}
func (s *SQLStore) getNextPreKeyID() (uint32, error) {
var lastKeyID sql.NullInt32
err := s.db.QueryRow(getLastPreKeyIDQuery, s.JID).Scan(&lastKeyID)
if err != nil {
return 0, fmt.Errorf("failed to query next prekey ID: %w", err)
}
return uint32(lastKeyID.Int32) + 1, nil
}
func (s *SQLStore) GenOnePreKey() (*keys.PreKey, error) {
s.preKeyLock.Lock()
defer s.preKeyLock.Unlock()
nextKeyID, err := s.getNextPreKeyID()
if err != nil {
return nil, err
}
return s.genOnePreKey(nextKeyID, true)
}
func (s *SQLStore) GetOrGenPreKeys(count uint32) ([]*keys.PreKey, error) {
s.preKeyLock.Lock()
defer s.preKeyLock.Unlock()
res, err := s.db.Query(getUnuploadedPreKeysQuery, s.JID, count)
if err != nil {
return nil, fmt.Errorf("failed to query existing prekeys: %w", err)
}
newKeys := make([]*keys.PreKey, count)
var existingCount uint32
for res.Next() {
var key *keys.PreKey
key, err = scanPreKey(res)
if err != nil {
return nil, err
} else if key != nil {
newKeys[existingCount] = key
existingCount++
}
}
if existingCount < uint32(len(newKeys)) {
var nextKeyID uint32
nextKeyID, err = s.getNextPreKeyID()
if err != nil {
return nil, err
}
for i := existingCount; i < count; i++ {
newKeys[i], err = s.genOnePreKey(nextKeyID, false)
if err != nil {
return nil, fmt.Errorf("failed to generate prekey: %w", err)
}
nextKeyID++
}
}
return newKeys, nil
}
func scanPreKey(row scannable) (*keys.PreKey, error) {
var priv []byte
var id uint32
err := row.Scan(&id, &priv)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else if err != nil {
return nil, err
} else if len(priv) != 32 {
return nil, ErrInvalidLength
}
return &keys.PreKey{
KeyPair: *keys.NewKeyPairFromPrivateKey(*(*[32]byte)(priv)),
KeyID: id,
}, nil
}
func (s *SQLStore) GetPreKey(id uint32) (*keys.PreKey, error) {
return scanPreKey(s.db.QueryRow(getPreKeyQuery, s.JID, id))
}
func (s *SQLStore) RemovePreKey(id uint32) error {
_, err := s.db.Exec(deletePreKeyQuery, s.JID, id)
return err
}
func (s *SQLStore) MarkPreKeysAsUploaded(upToID uint32) error {
_, err := s.db.Exec(markPreKeysAsUploadedQuery, s.JID, upToID)
return err
}
func (s *SQLStore) UploadedPreKeyCount() (count int, err error) {
err = s.db.QueryRow(getUploadedPreKeyCountQuery, s.JID).Scan(&count)
return
}
const (
getSenderKeyQuery = `SELECT sender_key FROM whatsmeow_sender_keys WHERE our_jid=$1 AND chat_id=$2 AND sender_id=$3`
putSenderKeyQuery = `
INSERT INTO whatsmeow_sender_keys (our_jid, chat_id, sender_id, sender_key) VALUES ($1, $2, $3, $4)
ON CONFLICT (our_jid, chat_id, sender_id) DO UPDATE SET sender_key=$4
`
)
func (s *SQLStore) PutSenderKey(group, user string, session []byte) error {
_, err := s.db.Exec(putSenderKeyQuery, s.JID, group, user, session)
return err
}
func (s *SQLStore) GetSenderKey(group, user string) (key []byte, err error) {
err = s.db.QueryRow(getSenderKeyQuery, s.JID, group, user).Scan(&key)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
const (
putAppStateSyncKeyQuery = `
INSERT INTO whatsmeow_app_state_sync_keys (jid, key_id, key_data, timestamp, fingerprint) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (jid, key_id) DO UPDATE SET key_data=$3, timestamp=$4, fingerprint=$5
`
getAppStateSyncKeyQuery = `SELECT key_data, timestamp, fingerprint FROM whatsmeow_app_state_sync_keys WHERE jid=$1 AND key_id=$2`
)
func (s *SQLStore) PutAppStateSyncKey(id []byte, key store.AppStateSyncKey) error {
_, err := s.db.Exec(putAppStateSyncKeyQuery, s.JID, id, key.Data, key.Timestamp, key.Fingerprint)
return err
}
func (s *SQLStore) GetAppStateSyncKey(id []byte) (*store.AppStateSyncKey, error) {
var key store.AppStateSyncKey
err := s.db.QueryRow(getAppStateSyncKeyQuery, s.JID, id).Scan(&key.Data, &key.Timestamp, &key.Fingerprint)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return &key, err
}
const (
putAppStateVersionQuery = `
INSERT INTO whatsmeow_app_state_version (jid, name, version, hash) VALUES ($1, $2, $3, $4)
ON CONFLICT (jid, name) DO UPDATE SET version=$3, hash=$4
`
getAppStateVersionQuery = `SELECT version, hash FROM whatsmeow_app_state_version WHERE jid=$1 AND name=$2`
deleteAppStateVersionQuery = `DELETE FROM whatsmeow_app_state_version WHERE jid=$1 AND name=$2`
putAppStateMutationMACsQuery = `INSERT INTO whatsmeow_app_state_mutation_macs (jid, name, version, index_mac, value_mac) VALUES `
deleteAppStateMutationMACsQueryPostgres = `DELETE FROM whatsmeow_app_state_mutation_macs WHERE jid=$1 AND name=$2 AND index_mac=ANY($3::bytea[])`
deleteAppStateMutationMACsQueryGeneric = `DELETE FROM whatsmeow_app_state_mutation_macs WHERE jid=$1 AND name=$2 AND index_mac IN `
getAppStateMutationMACQuery = `SELECT value_mac FROM whatsmeow_app_state_mutation_macs WHERE jid=$1 AND name=$2 AND index_mac=$3 ORDER BY version DESC LIMIT 1`
)
func (s *SQLStore) PutAppStateVersion(name string, version uint64, hash [128]byte) error {
_, err := s.db.Exec(putAppStateVersionQuery, s.JID, name, version, hash[:])
return err
}
func (s *SQLStore) GetAppStateVersion(name string) (version uint64, hash [128]byte, err error) {
var uncheckedHash []byte
err = s.db.QueryRow(getAppStateVersionQuery, s.JID, name).Scan(&version, &uncheckedHash)
if errors.Is(err, sql.ErrNoRows) {
// version will be 0 and hash will be an empty array, which is the correct initial state
err = nil
} else if err != nil {
// There's an error, just return it
} else if len(uncheckedHash) != 128 {
// This shouldn't happen
err = ErrInvalidLength
} else {
// No errors, convert hash slice to array
hash = *(*[128]byte)(uncheckedHash)
}
return
}
func (s *SQLStore) DeleteAppStateVersion(name string) error {
_, err := s.db.Exec(deleteAppStateVersionQuery, s.JID, name)
return err
}
type execable interface {
Exec(query string, args ...interface{}) (sql.Result, error)
}
func (s *SQLStore) putAppStateMutationMACs(tx execable, name string, version uint64, mutations []store.AppStateMutationMAC) error {
values := make([]interface{}, 3+len(mutations)*2)
queryParts := make([]string, len(mutations))
values[0] = s.JID
values[1] = name
values[2] = version
for i, mutation := range mutations {
baseIndex := 3 + i*2
values[baseIndex] = mutation.IndexMAC
values[baseIndex+1] = mutation.ValueMAC
if s.dialect == "sqlite3" {
queryParts[i] = fmt.Sprintf("(?1, ?2, ?3, ?%d, ?%d)", baseIndex+1, baseIndex+2)
} else {
queryParts[i] = fmt.Sprintf("($1, $2, $3, $%d, $%d)", baseIndex+1, baseIndex+2)
}
}
_, err := tx.Exec(putAppStateMutationMACsQuery+strings.Join(queryParts, ","), values...)
return err
}
const mutationBatchSize = 400
func (s *SQLStore) PutAppStateMutationMACs(name string, version uint64, mutations []store.AppStateMutationMAC) error {
if len(mutations) > mutationBatchSize {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
for i := 0; i < len(mutations); i += mutationBatchSize {
var mutationSlice []store.AppStateMutationMAC
if len(mutations) > i+mutationBatchSize {
mutationSlice = mutations[i : i+mutationBatchSize]
} else {
mutationSlice = mutations[i:]
}
err = s.putAppStateMutationMACs(tx, name, version, mutationSlice)
if err != nil {
_ = tx.Rollback()
return err
}
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
} else if len(mutations) > 0 {
return s.putAppStateMutationMACs(s.db, name, version, mutations)
}
return nil
}
func (s *SQLStore) DeleteAppStateMutationMACs(name string, indexMACs [][]byte) (err error) {
if len(indexMACs) == 0 {
return
}
if s.dialect == "postgres" && PostgresArrayWrapper != nil {
_, err = s.db.Exec(deleteAppStateMutationMACsQueryPostgres, s.JID, name, PostgresArrayWrapper(indexMACs))
} else {
args := make([]interface{}, 2+len(indexMACs))
args[0] = s.JID
args[1] = name
queryParts := make([]string, len(indexMACs))
for i, item := range indexMACs {
args[2+i] = item
queryParts[i] = fmt.Sprintf("$%d", i+3)
}
_, err = s.db.Exec(deleteAppStateMutationMACsQueryGeneric+"("+strings.Join(queryParts, ",")+")", args...)
}
return
}
func (s *SQLStore) GetAppStateMutationMAC(name string, indexMAC []byte) (valueMAC []byte, err error) {
err = s.db.QueryRow(getAppStateMutationMACQuery, s.JID, name, indexMAC).Scan(&valueMAC)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
const (
putContactNameQuery = `
INSERT INTO whatsmeow_contacts (our_jid, their_jid, first_name, full_name) VALUES ($1, $2, $3, $4)
ON CONFLICT (our_jid, their_jid) DO UPDATE SET first_name=$3, full_name=$4
`
putPushNameQuery = `
INSERT INTO whatsmeow_contacts (our_jid, their_jid, push_name) VALUES ($1, $2, $3)
ON CONFLICT (our_jid, their_jid) DO UPDATE SET push_name=$3
`
putBusinessNameQuery = `
INSERT INTO whatsmeow_contacts (our_jid, their_jid, business_name) VALUES ($1, $2, $3)
ON CONFLICT (our_jid, their_jid) DO UPDATE SET business_name=$3
`
getContactQuery = `
SELECT first_name, full_name, push_name, business_name FROM whatsmeow_contacts WHERE our_jid=$1 AND their_jid=$2
`
getAllContactsQuery = `
SELECT their_jid, first_name, full_name, push_name, business_name FROM whatsmeow_contacts WHERE our_jid=$1
`
)
func (s *SQLStore) PutPushName(user types.JID, pushName string) (bool, string, error) {
s.contactCacheLock.Lock()
defer s.contactCacheLock.Unlock()
cached, err := s.getContact(user)
if err != nil {
return false, "", err
}
if cached.PushName != pushName {
_, err = s.db.Exec(putPushNameQuery, s.JID, user, pushName)
if err != nil {
return false, "", err
}
previousName := cached.PushName
cached.PushName = pushName
cached.Found = true
return true, previousName, nil
}
return false, "", nil
}
func (s *SQLStore) PutBusinessName(user types.JID, businessName string) error {
s.contactCacheLock.Lock()
defer s.contactCacheLock.Unlock()
cached, err := s.getContact(user)
if err != nil {
return err
}
if cached.BusinessName != businessName {
_, err = s.db.Exec(putBusinessNameQuery, s.JID, user, businessName)
if err != nil {
return err
}
cached.BusinessName = businessName
cached.Found = true
}
return nil
}
func (s *SQLStore) PutContactName(user types.JID, firstName, fullName string) error {
s.contactCacheLock.Lock()
defer s.contactCacheLock.Unlock()
cached, err := s.getContact(user)
if err != nil {
return err
}
if cached.FirstName != firstName || cached.FullName != fullName {
_, err = s.db.Exec(putContactNameQuery, s.JID, user, firstName, fullName)
if err != nil {
return err
}
cached.FirstName = firstName
cached.FullName = fullName
cached.Found = true
}
return nil
}
func (s *SQLStore) getContact(user types.JID) (*types.ContactInfo, error) {
cached, ok := s.contactCache[user]
if ok {
return cached, nil
}
var first, full, push, business sql.NullString
err := s.db.QueryRow(getContactQuery, s.JID, user).Scan(&first, &full, &push, &business)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
info := &types.ContactInfo{
Found: err == nil,
FirstName: first.String,
FullName: full.String,
PushName: push.String,
BusinessName: business.String,
}
s.contactCache[user] = info
return info, nil
}
func (s *SQLStore) GetContact(user types.JID) (types.ContactInfo, error) {
s.contactCacheLock.Lock()
info, err := s.getContact(user)
s.contactCacheLock.Unlock()
if err != nil {
return types.ContactInfo{}, err
}
return *info, nil
}
func (s *SQLStore) GetAllContacts() (map[types.JID]types.ContactInfo, error) {
s.contactCacheLock.Lock()
defer s.contactCacheLock.Unlock()
rows, err := s.db.Query(getAllContactsQuery, s.JID)
if err != nil {
return nil, err
}
output := make(map[types.JID]types.ContactInfo, len(s.contactCache))
for rows.Next() {
var jid types.JID
var first, full, push, business sql.NullString
err = rows.Scan(&jid, &first, &full, &push, &business)
if err != nil {
return nil, fmt.Errorf("error scanning row: %w", err)
}
info := types.ContactInfo{
Found: true,
FirstName: first.String,
FullName: full.String,
PushName: push.String,
BusinessName: business.String,
}
output[jid] = info
s.contactCache[jid] = &info
}
return output, nil
}
const (
putChatSettingQuery = `
INSERT INTO whatsmeow_chat_settings (our_jid, chat_jid, %[1]s) VALUES ($1, $2, $3)
ON CONFLICT (our_jid, chat_jid) DO UPDATE SET %[1]s=$3
`
getChatSettingsQuery = `
SELECT muted_until, pinned, archived FROM whatsmeow_chat_settings WHERE our_jid=$1 AND chat_jid=$2
`
)
func (s *SQLStore) PutMutedUntil(chat types.JID, mutedUntil time.Time) error {
var val int64
if !mutedUntil.IsZero() {
val = mutedUntil.Unix()
}
_, err := s.db.Exec(fmt.Sprintf(putChatSettingQuery, "muted_until"), s.JID, chat, val)
return err
}
func (s *SQLStore) PutPinned(chat types.JID, pinned bool) error {
_, err := s.db.Exec(fmt.Sprintf(putChatSettingQuery, "pinned"), s.JID, chat, pinned)
return err
}
func (s *SQLStore) PutArchived(chat types.JID, archived bool) error {
_, err := s.db.Exec(fmt.Sprintf(putChatSettingQuery, "archived"), s.JID, chat, archived)
return err
}
func (s *SQLStore) GetChatSettings(chat types.JID) (settings types.LocalChatSettings, err error) {
var mutedUntil int64
err = s.db.QueryRow(getChatSettingsQuery, s.JID, chat).Scan(&mutedUntil, &settings.Pinned, &settings.Archived)
if errors.Is(err, sql.ErrNoRows) {
err = nil
} else if err != nil {
return
} else {
settings.Found = true
}
if mutedUntil != 0 {
settings.MutedUntil = time.Unix(mutedUntil, 0)
}
return
}

View File

@ -0,0 +1,214 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package sqlstore
import (
"database/sql"
)
type upgradeFunc func(*sql.Tx, *Container) error
// Upgrades is a list of functions that will upgrade a database to the latest version.
//
// 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}
func (c *Container) getVersion() (int, error) {
_, err := c.db.Exec("CREATE TABLE IF NOT EXISTS whatsmeow_version (version INTEGER)")
if err != nil {
return -1, err
}
version := 0
row := c.db.QueryRow("SELECT version FROM whatsmeow_version LIMIT 1")
if row != nil {
_ = row.Scan(&version)
}
return version, nil
}
func (c *Container) setVersion(tx *sql.Tx, version int) error {
_, err := tx.Exec("DELETE FROM whatsmeow_version")
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO whatsmeow_version (version) VALUES ($1)", version)
return err
}
// Upgrade upgrades the database from the current to the latest version available.
func (c *Container) Upgrade() error {
version, err := c.getVersion()
if err != nil {
return err
}
for ; version < len(Upgrades); version++ {
var tx *sql.Tx
tx, err = c.db.Begin()
if err != nil {
return err
}
migrateFunc := Upgrades[version]
err = migrateFunc(tx, c)
if err != nil {
_ = tx.Rollback()
return err
}
if err = c.setVersion(tx, version+1); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
}
return nil
}
func upgradeV1(tx *sql.Tx, _ *Container) error {
_, err := tx.Exec(`CREATE TABLE whatsmeow_device (
jid TEXT PRIMARY KEY,
registration_id BIGINT NOT NULL CHECK ( registration_id >= 0 AND registration_id < 4294967296 ),
noise_key bytea NOT NULL CHECK ( length(noise_key) = 32 ),
identity_key bytea NOT NULL CHECK ( length(identity_key) = 32 ),
signed_pre_key bytea NOT NULL CHECK ( length(signed_pre_key) = 32 ),
signed_pre_key_id INTEGER NOT NULL CHECK ( signed_pre_key_id >= 0 AND signed_pre_key_id < 16777216 ),
signed_pre_key_sig bytea NOT NULL CHECK ( length(signed_pre_key_sig) = 64 ),
adv_key bytea NOT NULL,
adv_details bytea NOT NULL,
adv_account_sig bytea NOT NULL CHECK ( length(adv_account_sig) = 64 ),
adv_device_sig bytea NOT NULL CHECK ( length(adv_device_sig) = 64 ),
platform TEXT NOT NULL DEFAULT '',
business_name TEXT NOT NULL DEFAULT '',
push_name TEXT NOT NULL DEFAULT ''
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_identity_keys (
our_jid TEXT,
their_id TEXT,
identity bytea NOT NULL CHECK ( length(identity) = 32 ),
PRIMARY KEY (our_jid, their_id),
FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_pre_keys (
jid TEXT,
key_id INTEGER CHECK ( key_id >= 0 AND key_id < 16777216 ),
key bytea NOT NULL CHECK ( length(key) = 32 ),
uploaded BOOLEAN NOT NULL,
PRIMARY KEY (jid, key_id),
FOREIGN KEY (jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_sessions (
our_jid TEXT,
their_id TEXT,
session bytea,
PRIMARY KEY (our_jid, their_id),
FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_sender_keys (
our_jid TEXT,
chat_id TEXT,
sender_id TEXT,
sender_key bytea NOT NULL,
PRIMARY KEY (our_jid, chat_id, sender_id),
FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_app_state_sync_keys (
jid TEXT,
key_id bytea,
key_data bytea NOT NULL,
timestamp BIGINT NOT NULL,
fingerprint bytea NOT NULL,
PRIMARY KEY (jid, key_id),
FOREIGN KEY (jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_app_state_version (
jid TEXT,
name TEXT,
version BIGINT NOT NULL,
hash bytea NOT NULL CHECK ( length(hash) = 128 ),
PRIMARY KEY (jid, name),
FOREIGN KEY (jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_app_state_mutation_macs (
jid TEXT,
name TEXT,
version BIGINT,
index_mac bytea CHECK ( length(index_mac) = 32 ),
value_mac bytea NOT NULL CHECK ( length(value_mac) = 32 ),
PRIMARY KEY (jid, name, version, index_mac),
FOREIGN KEY (jid, name) REFERENCES whatsmeow_app_state_version(jid, name) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_contacts (
our_jid TEXT,
their_jid TEXT,
first_name TEXT,
full_name TEXT,
push_name TEXT,
business_name TEXT,
PRIMARY KEY (our_jid, their_jid),
FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE whatsmeow_chat_settings (
our_jid TEXT,
chat_jid TEXT,
muted_until BIGINT NOT NULL DEFAULT 0,
pinned BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (our_jid, chat_jid),
FOREIGN KEY (our_jid) REFERENCES whatsmeow_device(jid) ON DELETE CASCADE ON UPDATE CASCADE
)`)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,132 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package store contains interfaces for storing data needed for WhatsApp multidevice.
package store
import (
"time"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
)
type IdentityStore interface {
PutIdentity(address string, key [32]byte) error
DeleteAllIdentities(phone string) error
DeleteIdentity(address string) error
IsTrustedIdentity(address string, key [32]byte) (bool, error)
}
type SessionStore interface {
GetSession(address string) ([]byte, error)
HasSession(address string) (bool, error)
PutSession(address string, session []byte) error
DeleteAllSessions(phone string) error
DeleteSession(address string) error
}
type PreKeyStore interface {
GetOrGenPreKeys(count uint32) ([]*keys.PreKey, error)
GenOnePreKey() (*keys.PreKey, error)
GetPreKey(id uint32) (*keys.PreKey, error)
RemovePreKey(id uint32) error
MarkPreKeysAsUploaded(upToID uint32) error
UploadedPreKeyCount() (int, error)
}
type SenderKeyStore interface {
PutSenderKey(group, user string, session []byte) error
GetSenderKey(group, user string) ([]byte, error)
}
type AppStateSyncKey struct {
Data []byte
Fingerprint []byte
Timestamp int64
}
type AppStateSyncKeyStore interface {
PutAppStateSyncKey(id []byte, key AppStateSyncKey) error
GetAppStateSyncKey(id []byte) (*AppStateSyncKey, error)
}
type AppStateMutationMAC struct {
IndexMAC []byte
ValueMAC []byte
}
type AppStateStore interface {
PutAppStateVersion(name string, version uint64, hash [128]byte) error
GetAppStateVersion(name string) (uint64, [128]byte, error)
DeleteAppStateVersion(name string) error
PutAppStateMutationMACs(name string, version uint64, mutations []AppStateMutationMAC) error
DeleteAppStateMutationMACs(name string, indexMACs [][]byte) error
GetAppStateMutationMAC(name string, indexMAC []byte) (valueMAC []byte, err error)
}
type ContactStore interface {
PutPushName(user types.JID, pushName string) (bool, string, error)
PutBusinessName(user types.JID, businessName string) error
PutContactName(user types.JID, fullName, firstName string) error
GetContact(user types.JID) (types.ContactInfo, error)
GetAllContacts() (map[types.JID]types.ContactInfo, error)
}
type ChatSettingsStore interface {
PutMutedUntil(chat types.JID, mutedUntil time.Time) error
PutPinned(chat types.JID, pinned bool) error
PutArchived(chat types.JID, archived bool) error
GetChatSettings(chat types.JID) (types.LocalChatSettings, error)
}
type DeviceContainer interface {
PutDevice(store *Device) error
DeleteDevice(store *Device) error
}
type Device struct {
Log waLog.Logger
NoiseKey *keys.KeyPair
IdentityKey *keys.KeyPair
SignedPreKey *keys.PreKey
RegistrationID uint32
AdvSecretKey []byte
ID *types.JID
Account *waProto.ADVSignedDeviceIdentity
Platform string
BusinessName string
PushName string
Initialized bool
Identities IdentityStore
Sessions SessionStore
PreKeys PreKeyStore
SenderKeys SenderKeyStore
AppStateKeys AppStateSyncKeyStore
AppState AppStateStore
Contacts ContactStore
ChatSettings ChatSettingsStore
Container DeviceContainer
}
func (device *Device) Save() error {
return device.Container.PutDevice(device)
}
func (device *Device) Delete() error {
err := device.Container.DeleteDevice(device)
if err != nil {
return err
}
device.ID = nil
return nil
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types
import "time"
type BasicCallMeta struct {
From JID
Timestamp time.Time
CallCreator JID
CallID string
}
type CallRemoteMeta struct {
RemotePlatform string // The platform of the caller's WhatsApp client
RemoteVersion string // Version of the caller's WhatsApp client
}

View File

@ -0,0 +1,103 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package events
import (
"time"
"go.mau.fi/whatsmeow/appstate"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
)
// Contact is emitted when an entry in the user's contact list is modified from another device.
type Contact struct {
JID types.JID // The contact who was modified.
Timestamp time.Time // The time when the modification happened.'
Action *waProto.ContactAction // The new contact info.
}
// PushName is emitted when a message is received with a different push name than the previous value cached for the same user.
type PushName struct {
JID types.JID // The user whose push name changed.
Message *types.MessageInfo // The message where this change was first noticed.
OldPushName string // The previous push name from the local cache.
NewPushName string // The new push name that was included in the message.
}
// Pin is emitted when a chat is pinned or unpinned from another device.
type Pin struct {
JID types.JID // The chat which was pinned or unpinned.
Timestamp time.Time // The time when the (un)pinning happened.
Action *waProto.PinAction // Whether the chat is now pinned or not.
}
// Star is emitted when a message is starred or unstarred from another device.
type Star struct {
ChatJID types.JID // The chat where the message was pinned.
SenderJID types.JID // In group chats, the user who sent the message (except if the message was sent by the user).
IsFromMe bool // Whether the message was sent by the user.
MessageID string // The message which was starred or unstarred.
Timestamp time.Time // The time when the (un)starring happened.
Action *waProto.StarAction // Whether the message is now starred or not.
}
// DeleteForMe is emitted when a message is deleted (for the current user only) from another device.
type DeleteForMe struct {
ChatJID types.JID // The chat where the message was deleted.
SenderJID types.JID // In group chats, the user who sent the message (except if the message was sent by the user).
IsFromMe bool // Whether the message was sent by the user.
MessageID string // The message which was deleted.
Timestamp time.Time // The time when the deletion happened.
Action *waProto.DeleteMessageForMeAction // Additional information for the deletion.
}
// Mute is emitted when a chat is muted or unmuted from another device.
type Mute struct {
JID types.JID // The chat which was muted or unmuted.
Timestamp time.Time // The time when the (un)muting happened.
Action *waProto.MuteAction // The current mute status of the chat.
}
// Archive is emitted when a chat is archived or unarchived from another device.
type Archive struct {
JID types.JID // The chat which was archived or unarchived.
Timestamp time.Time // The time when the (un)archiving happened.
Action *waProto.ArchiveChatAction // The current archival status of the chat.
}
// PushNameSetting is emitted when the user's push name is changed from another device.
type PushNameSetting struct {
Timestamp time.Time // The time when the push name was changed.
Action *waProto.PushNameSetting // The new push name for the user.
}
// UnarchiveChatsSetting is emitted when the user changes the "Keep chats archived" setting from another device.
type UnarchiveChatsSetting struct {
Timestamp time.Time // The time when the setting was changed.
Action *waProto.UnarchiveChatsSetting // The new settings.
}
// AppState is emitted directly for new data received from app state syncing.
// You should generally use the higher-level events like events.Contact and events.Mute.
type AppState struct {
Index []string
*waProto.SyncActionValue
}
// AppStateSyncComplete is emitted when app state is resynced.
type AppStateSyncComplete struct {
Name appstate.WAPatchName
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package events
import (
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
// CallOffer is emitted when the user receives a call on WhatsApp.
type CallOffer struct {
types.BasicCallMeta
types.CallRemoteMeta
Data *waBinary.Node // The call offer data
}
// CallAccept is emitted when a call is accepted on WhatsApp.
type CallAccept struct {
types.BasicCallMeta
types.CallRemoteMeta
Data *waBinary.Node
}
// CallOfferNotice is emitted when the user receives a notice of a call on WhatsApp.
// This seems to be primarily for group calls (whereas CallOffer is for 1:1 calls).
type CallOfferNotice struct {
types.BasicCallMeta
Media string // "audio" or "video" depending on call type
Type string // "group" when it's a group call
Data *waBinary.Node
}
// CallRelayLatency is emitted slightly after the user receives a call on WhatsApp.
type CallRelayLatency struct {
types.BasicCallMeta
Data *waBinary.Node
}
// CallTerminate is emitted when the other party terminates a call on WhatsApp.
type CallTerminate struct {
types.BasicCallMeta
Reason string
Data *waBinary.Node
}
// UnknownCallEvent is emitted when a call element with unknown content is received.
type UnknownCallEvent struct {
Node *waBinary.Node
}

View File

@ -0,0 +1,263 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package events contains all the events that whatsmeow.Client emits to functions registered with AddEventHandler.
package events
import (
"fmt"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
)
// QR is emitted after connecting when there's no session data in the device store.
//
// The QR codes are available in the Codes slice. You should render the strings as QR codes one by
// one, switching to the next one whenever enough time has passed. WhatsApp web seems to show the
// first code for 60 seconds and all other codes for 20 seconds.
//
// When the QR code has been scanned and pairing is complete, PairSuccess will be emitted. If you
// run out of codes before scanning, the server will close the websocket, and you will have to
// reconnect to get more codes.
type QR struct {
Codes []string
}
// PairSuccess is emitted after the QR code has been scanned with the phone and the handshake has
// been completed. Note that this is generally followed by a websocket reconnection, so you should
// wait for the Connected before trying to send anything.
type PairSuccess struct {
ID types.JID
BusinessName string
Platform string
}
// PairError is emitted when a pair-success event is received from the server, but finishing the pairing locally fails.
type PairError struct {
ID types.JID
BusinessName string
Platform string
Error error
}
// QRScannedWithoutMultidevice is emitted when the pairing QR code is scanned, but the phone didn't have multidevice enabled.
// The same QR code can still be scanned after this event, which means the user can just be told to enable multidevice and re-scan the code.
type QRScannedWithoutMultidevice struct{}
// Connected is emitted when the client has successfully connected to the WhatsApp servers
// and is authenticated. The user who the client is authenticated as will be in the device store
// at this point, which is why this event doesn't contain any data.
type Connected struct{}
// LoggedOut is emitted when the client has been unpaired from the phone.
//
// This can happen while connected (stream:error messages) or right after connecting (connect failure messages).
type LoggedOut struct {
// OnConnect is true if the event was triggered by a connect failure message.
// If it's false, the event was triggered by a stream:error message.
OnConnect bool
}
// StreamReplaced is emitted when the client is disconnected by another client connecting with the same keys.
//
// This can happen if you accidentally start another process with the same session
// or otherwise try to connect twice with the same session.
type StreamReplaced struct{}
// ConnectFailure is emitted when the WhatsApp server sends a <failure> node with an unknown reason.
//
// Known reasons are handled internally and emitted as different events (e.g. LoggedOut).
type ConnectFailure struct {
Reason string
Raw *waBinary.Node
}
// StreamError is emitted when the WhatsApp server sends a <stream:error> node with an unknown code.
//
// Known codes are handled internally and emitted as different events (e.g. LoggedOut).
type StreamError struct {
Code string
Raw *waBinary.Node
}
// Disconnected is emitted when the websocket is closed by the server.
type Disconnected struct{}
// HistorySync is emitted when the phone has sent a blob of historical messages.
type HistorySync struct {
Data *waProto.HistorySync
}
// UndecryptableMessage is emitted when receiving a new message that failed to decrypt.
//
// The library will automatically ask the sender to retry. If the sender resends the message,
// and it's decryptable, then it will be emitted as a normal Message event.
//
// The UndecryptableMessage event may also be repeated if the resent message is also undecryptable.
type UndecryptableMessage struct {
Info types.MessageInfo
// IsUnavailable is true if the recipient device didn't send a ciphertext to this device at all
// (as opposed to sending a ciphertext, but the ciphertext not being decryptable).
IsUnavailable bool
}
// Message is emitted when receiving a new message.
type Message struct {
Info types.MessageInfo // Information about the message like the chat and sender IDs
Message *waProto.Message // The actual message struct
IsEphemeral bool // True if the message was unwrapped from an EphemeralMessage
IsViewOnce bool // True if the message was unwrapped from a ViewOnceMessage
// The raw message struct. This is the raw unmodified data, which means the actual message might
// be wrapped in DeviceSentMessage, EphemeralMessage or ViewOnceMessage.
RawMessage *waProto.Message
}
// ReceiptType represents the type of a Receipt event.
type ReceiptType string
const (
// ReceiptTypeDelivered means the message was delivered to the device (but the user might not have noticed).
ReceiptTypeDelivered ReceiptType = ""
// ReceiptTypeRetry means the message was delivered to the device, but decrypting the message failed.
ReceiptTypeRetry ReceiptType = "retry"
// ReceiptTypeRead means the user opened the chat and saw the message.
ReceiptTypeRead ReceiptType = "read"
// ReceiptTypeReadSelf means the current user read a message from a different device, and has read receipts disabled in privacy settings.
ReceiptTypeReadSelf ReceiptType = "read-self"
)
// GoString returns the name of the Go constant for the ReceiptType value.
func (rt ReceiptType) GoString() string {
switch rt {
case ReceiptTypeRead:
return "events.ReceiptTypeRead"
case ReceiptTypeReadSelf:
return "events.ReceiptTypeReadSelf"
case ReceiptTypeDelivered:
return "events.ReceiptTypeDelivered"
default:
return fmt.Sprintf("events.ReceiptType(%#v)", string(rt))
}
}
// Receipt is emitted when an outgoing message is delivered to or read by another user, or when another device reads an incoming message.
//
// N.B. WhatsApp on Android sends message IDs from newest message to oldest, but WhatsApp on iOS sends them in the opposite order (oldest first).
type Receipt struct {
types.MessageSource
MessageIDs []types.MessageID
Timestamp time.Time
Type ReceiptType
}
// ChatPresence is emitted when a chat state update (also known as typing notification) is received.
//
// Note that WhatsApp won't send you these updates unless you mark yourself as online:
// client.SendPresence(types.PresenceAvailable)
type ChatPresence struct {
types.MessageSource
State types.ChatPresence
}
// Presence is emitted when a presence update is received.
//
// Note that WhatsApp only sends you presence updates for individual users after you subscribe to them:
// client.SubscribePresence(user JID)
type Presence struct {
// The user whose presence event this is
From types.JID
// True if the user is now offline
Unavailable bool
// The time when the user was last online. This may be the zero value if the user has hid their last seen time.
LastSeen time.Time
}
// JoinedGroup is emitted when you join or are added to a group.
type JoinedGroup struct {
Reason string // If the event was triggered by you using an invite link, this will be "invite"
types.GroupInfo
}
// GroupInfo is emitted when the metadata of a group changes.
type GroupInfo struct {
JID types.JID // The group ID in question
Notify string // Seems like a top-level type for the invite
Sender *types.JID // The user who made the change. Doesn't seem to be present when notify=invite
Timestamp time.Time // The time when the change occurred
Name *types.GroupName // Group name change
Topic *types.GroupTopic // Group topic (description) change
Locked *types.GroupLocked // Group locked status change (can only admins edit group info?)
Announce *types.GroupAnnounce // Group announce status change (can only admins send messages?)
Ephemeral *types.GroupEphemeral // Disappearing messages change
NewInviteLink *string // Group invite link change
PrevParticipantVersionID string
ParticipantVersionID string
JoinReason string // This will be "invite" if the user joined via invite link
Join []types.JID // Users who joined or were added the group
Leave []types.JID // Users who left or were removed from the group
Promote []types.JID // Users who were promoted to admins
Demote []types.JID // Users who were demoted to normal users
UnknownChanges []*waBinary.Node
}
// Picture is emitted when a user's profile picture or group's photo is changed.
//
// You can use Client.GetProfilePictureInfo to get the actual image URL after this event.
type Picture struct {
JID types.JID // The user or group ID where the picture was changed.
Author types.JID // The user who changed the picture.
Timestamp time.Time // The timestamp when the picture was changed.
Remove bool // True if the picture was removed.
PictureID string // The new picture ID if it was not removed.
}
// IdentityChange is emitted when another user changes their primary device.
type IdentityChange struct {
JID types.JID
Timestamp time.Time
// Implicit will be set to true if the event was triggered by an untrusted identity error,
// rather than an identity change notification from the server.
Implicit bool
}
// PrivacySettings is emitted when the user changes their privacy settings.
type PrivacySettings struct {
NewSettings types.PrivacySettings
GroupAddChanged bool
LastSeenChanged bool
StatusChanged bool
ProfileChanged bool
ReadReceiptsChanged bool
}
// OfflineSyncPreview is emitted right after connecting if the server is going to send events that the client missed during downtime.
type OfflineSyncPreview struct {
Total int
AppDataChanges int
Messages int
Notifications int
Receipts int
}
// OfflineSyncCompleted is emitted after the server has finished sending missed events.
type OfflineSyncCompleted struct {
Count int
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types
import (
"time"
)
// GroupInfo contains basic information about a group chat on WhatsApp.
type GroupInfo struct {
JID JID
OwnerJID JID
GroupName
GroupTopic
GroupLocked
GroupAnnounce
GroupEphemeral
GroupCreated time.Time
ParticipantVersionID string
Participants []GroupParticipant
}
// GroupName contains the name of a group along with metadata of who set it and when.
type GroupName struct {
Name string
NameSetAt time.Time
NameSetBy JID
}
// GroupTopic contains the topic (description) of a group along with metadata of who set it and when.
type GroupTopic struct {
Topic string
TopicID string
TopicSetAt time.Time
TopicSetBy JID
}
// GroupLocked specifies whether the group info can only be edited by admins.
type GroupLocked struct {
IsLocked bool
}
// GroupAnnounce specifies whether only admins can send messages in the group.
type GroupAnnounce struct {
IsAnnounce bool
AnnounceVersionID string
}
// GroupParticipant contains info about a participant of a WhatsApp group chat.
type GroupParticipant struct {
JID JID
IsAdmin bool
IsSuperAdmin bool
}
// GroupEphemeral contains the group's disappearing messages settings.
type GroupEphemeral struct {
IsEphemeral bool
DisappearingTimer uint32
}

193
vendor/go.mau.fi/whatsmeow/types/jid.go vendored Normal file
View File

@ -0,0 +1,193 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package types contains various structs and other types used by whatsmeow.
package types
import (
"database/sql"
"database/sql/driver"
"fmt"
"strconv"
"strings"
signalProtocol "go.mau.fi/libsignal/protocol"
)
// Known JID servers on WhatsApp
const (
DefaultUserServer = "s.whatsapp.net"
GroupServer = "g.us"
LegacyUserServer = "c.us"
BroadcastServer = "broadcast"
)
// Some JIDs that are contacted often.
var (
EmptyJID = JID{}
GroupServerJID = NewJID("", GroupServer)
ServerJID = NewJID("", DefaultUserServer)
BroadcastServerJID = NewJID("", BroadcastServer)
StatusBroadcastJID = NewJID("status", BroadcastServer)
PSAJID = NewJID("0", LegacyUserServer)
OfficialBusinessJID = NewJID("16505361212", LegacyUserServer)
)
// MessageID is the internal ID of a WhatsApp message.
type MessageID = string
// JID represents a WhatsApp user ID.
//
// There are two types of JIDs: regular JID pairs (user and server) and AD-JIDs (user, agent and device).
// AD JIDs are only used to refer to specific devices of users, so the server is always s.whatsapp.net (DefaultUserServer).
// Regular JIDs can be used for entities on any servers (users, groups, broadcasts).
type JID struct {
User string
Agent uint8
Device uint8
Server string
AD bool
}
// UserInt returns the user as an integer. This is only safe to run on normal users, not on groups or broadcast lists.
func (jid JID) UserInt() uint64 {
number, _ := strconv.ParseUint(jid.User, 10, 64)
return number
}
// ToNonAD returns a version of the JID struct that doesn't have the agent and device set.
func (jid JID) ToNonAD() JID {
if jid.AD {
return JID{
User: jid.User,
Server: DefaultUserServer,
}
} else {
return jid
}
}
// SignalAddress returns the Signal protocol address for the user.
func (jid JID) SignalAddress() *signalProtocol.SignalAddress {
user := jid.User
if jid.Agent != 0 {
user = fmt.Sprintf("%s_%d", jid.User, jid.Agent)
}
return signalProtocol.NewSignalAddress(user, uint32(jid.Device))
}
// IsBroadcastList returns true if the JID is a broadcast list, but not the status broadcast.
func (jid JID) IsBroadcastList() bool {
return jid.Server == BroadcastServer && jid.User != StatusBroadcastJID.User
}
// NewADJID creates a new AD JID.
func NewADJID(user string, agent, device uint8) JID {
return JID{
User: user,
Agent: agent,
Device: device,
Server: DefaultUserServer,
AD: true,
}
}
func parseADJID(user string) (JID, error) {
var fullJID JID
fullJID.AD = true
fullJID.Server = DefaultUserServer
dotIndex := strings.IndexRune(user, '.')
colonIndex := strings.IndexRune(user, ':')
if dotIndex < 0 || colonIndex < 0 || colonIndex+1 <= dotIndex {
return fullJID, fmt.Errorf("failed to parse ADJID: missing separators")
}
fullJID.User = user[:dotIndex]
agent, err := strconv.Atoi(user[dotIndex+1 : colonIndex])
if err != nil {
return fullJID, fmt.Errorf("failed to parse agent from JID: %w", err)
} else if agent < 0 || agent > 255 {
return fullJID, fmt.Errorf("failed to parse agent from JID: invalid value (%d)", agent)
}
device, err := strconv.Atoi(user[colonIndex+1:])
if err != nil {
return fullJID, fmt.Errorf("failed to parse device from JID: %w", err)
} else if device < 0 || device > 255 {
return fullJID, fmt.Errorf("failed to parse device from JID: invalid value (%d)", device)
}
fullJID.Agent = uint8(agent)
fullJID.Device = uint8(device)
return fullJID, nil
}
// ParseJID parses a JID out of the given string. It supports both regular and AD JIDs.
func ParseJID(jid string) (JID, error) {
parts := strings.Split(jid, "@")
if len(parts) == 1 {
return NewJID("", parts[0]), nil
} else if strings.ContainsRune(parts[0], ':') && strings.ContainsRune(parts[0], '.') && parts[1] == DefaultUserServer {
return parseADJID(parts[0])
}
return NewJID(parts[0], parts[1]), nil
}
// NewJID creates a new regular JID.
func NewJID(user, server string) JID {
return JID{
User: user,
Server: server,
}
}
// String converts the JID to a string representation.
// The output string can be parsed with ParseJID, except for JIDs with no User part specified.
func (jid JID) String() string {
if jid.AD {
return fmt.Sprintf("%s.%d:%d@%s", jid.User, jid.Agent, jid.Device, jid.Server)
} else if len(jid.User) > 0 {
return fmt.Sprintf("%s@%s", jid.User, jid.Server)
} else {
return jid.Server
}
}
// IsEmpty returns true if the JID has no server (which is required for all JIDs).
func (jid JID) IsEmpty() bool {
return len(jid.Server) == 0
}
var _ sql.Scanner = (*JID)(nil)
// Scan scans the given SQL value into this JID.
func (jid *JID) Scan(src interface{}) error {
if src == nil {
return nil
}
var out JID
var err error
switch val := src.(type) {
case string:
out, err = ParseJID(val)
case []byte:
out, err = ParseJID(string(val))
default:
err = fmt.Errorf("unsupported type %T for scanning JID", val)
}
if err != nil {
return err
}
*jid = out
return nil
}
// Value returns the string representation of the JID as a value that the SQL package can use.
func (jid JID) Value() (driver.Value, error) {
if len(jid.Server) == 0 {
return nil, nil
}
return jid.String(), nil
}

View File

@ -0,0 +1,58 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types
import (
"fmt"
"time"
)
// MessageSource contains basic sender and chat information about a message.
type MessageSource struct {
Chat JID // The chat where the message was sent.
Sender JID // The user who sent the message.
IsFromMe bool // Whether the message was sent by the current user instead of someone else.
IsGroup bool // Whether the chat is a group chat or broadcast list.
// When sending a read receipt to a broadcast list message, the Chat is the broadcast list
// and Sender is you, so this field contains the recipient of the read receipt.
BroadcastListOwner JID
}
// IsIncomingBroadcast returns true if the message was sent to a broadcast list instead of directly to the user.
//
// If this is true, it means the message shows up in the direct chat with the Sender.
func (ms *MessageSource) IsIncomingBroadcast() bool {
return (!ms.IsFromMe || !ms.BroadcastListOwner.IsEmpty()) && ms.Chat.IsBroadcastList()
}
// DeviceSentMeta contains metadata from messages sent by another one of the user's own devices.
type DeviceSentMeta struct {
DestinationJID string // The destination user. This should match the MessageInfo.Recipient field.
Phash string
}
// MessageInfo contains metadata about an incoming message.
type MessageInfo struct {
MessageSource
ID string
Type string
PushName string
Timestamp time.Time
Category string
DeviceSentMeta *DeviceSentMeta // Metadata for direct messages sent from another one of the user's own devices.
}
// SourceString returns a log-friendly representation of who sent the message and where.
func (ms *MessageSource) SourceString() string {
if ms.Sender != ms.Chat {
return fmt.Sprintf("%s in %s", ms.Sender, ms.Chat)
} else {
return ms.Chat.String()
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types
type Presence string
const (
PresenceAvailable Presence = "available"
PresenceUnavailable Presence = "unavailable"
)
type ChatPresence string
const (
ChatPresenceComposing ChatPresence = "composing"
ChatPresenceRecording ChatPresence = "recording"
ChatPresencePaused ChatPresence = "paused"
)

View File

@ -0,0 +1,96 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types
import (
"time"
waProto "go.mau.fi/whatsmeow/binary/proto"
)
// VerifiedName contains verified WhatsApp business details.
type VerifiedName struct {
Certificate *waProto.VerifiedNameCertificate
Details *waProto.VerifiedNameDetails
}
// UserInfo contains info about a WhatsApp user.
type UserInfo struct {
VerifiedName *VerifiedName
Status string
PictureID string
Devices []JID
}
// ProfilePictureInfo contains the ID and URL for a WhatsApp user's profile picture or group's photo.
type ProfilePictureInfo struct {
URL string // The full URL for the image, can be downloaded with a simple HTTP request.
ID string // The ID of the image. This is the same as UserInfo.PictureID.
Type string // The type of image. Known types include "image" (full res) and "preview" (thumbnail).
DirectPath string // The path to the image, probably not very useful
}
// ContactInfo contains the cached names of a WhatsApp user.
type ContactInfo struct {
Found bool
FirstName string
FullName string
PushName string
BusinessName string
}
// LocalChatSettings contains the cached local settings for a chat.
type LocalChatSettings struct {
Found bool
MutedUntil time.Time
Pinned bool
Archived bool
}
// IsOnWhatsAppResponse contains information received in response to checking if a phone number is on WhatsApp.
type IsOnWhatsAppResponse struct {
Query string // The query string used
JID JID // The canonical user ID
IsIn bool // Whether the phone is registered or not.
VerifiedName *VerifiedName // If the phone is a business, the verified business details.
}
// BusinessMessageLinkTarget contains the info that is found using a business message link (see Client.ResolveBusinessMessageLink)
type BusinessMessageLinkTarget struct {
JID JID // The JID of the business.
PushName string // The notify / push name of the business.
VerifiedName string // The verified business name.
IsSigned bool // Some boolean, seems to be true?
VerifiedLevel string // I guess the level of verification, starting from "unknown".
Message string // The message that WhatsApp clients will pre-fill in the input box when clicking the link.
}
// PrivacySetting is an individual setting value in the user's privacy settings.
type PrivacySetting string
// Possible privacy setting values.
const (
PrivacySettingUndefined PrivacySetting = ""
PrivacySettingAll PrivacySetting = "all"
PrivacySettingContacts PrivacySetting = "contacts"
PrivacySettingNone PrivacySetting = "none"
)
// PrivacySettings contains the user's privacy settings.
type PrivacySettings struct {
GroupAdd PrivacySetting
LastSeen PrivacySetting
Status PrivacySetting
Profile PrivacySetting
ReadReceipts PrivacySetting
}

107
vendor/go.mau.fi/whatsmeow/upload.go vendored Normal file
View File

@ -0,0 +1,107 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"go.mau.fi/whatsmeow/socket"
"go.mau.fi/whatsmeow/util/cbcutil"
)
// UploadResponse contains the data from the attachment upload, which can be put into a message to send the attachment.
type UploadResponse struct {
URL string
DirectPath string
MediaKey []byte
FileEncSHA256 []byte
FileSHA256 []byte
FileLength uint64
}
// Upload uploads the given attachment to WhatsApp servers.
func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaType) (resp UploadResponse, err error) {
resp.FileLength = uint64(len(plaintext))
resp.MediaKey = make([]byte, 32)
_, err = rand.Read(resp.MediaKey)
if err != nil {
return
}
plaintextSHA256 := sha256.Sum256(plaintext)
resp.FileSHA256 = plaintextSHA256[:]
iv, cipherKey, macKey, _ := getMediaKeys(resp.MediaKey, appInfo)
var ciphertext []byte
ciphertext, err = cbcutil.Encrypt(cipherKey, iv, plaintext)
if err != nil {
err = fmt.Errorf("failed to encrypt file: %w", err)
return
}
h := hmac.New(sha256.New, macKey)
h.Write(iv)
h.Write(ciphertext)
dataToUpload := append(ciphertext, h.Sum(nil)[:10]...)
fileEncSHA256 := sha256.Sum256(dataToUpload)
resp.FileEncSHA256 = fileEncSHA256[:]
err = cli.refreshMediaConn(false)
if err != nil {
err = fmt.Errorf("failed to refresh media connections: %w", err)
return
}
token := base64.URLEncoding.EncodeToString(resp.FileEncSHA256)
q := url.Values{
"auth": []string{cli.mediaConn.Auth},
"token": []string{token},
}
mmsType := mediaTypeToMMSType[appInfo]
uploadURL := url.URL{
Scheme: "https",
Host: cli.mediaConn.Hosts[0].Hostname,
Path: fmt.Sprintf("/mms/%s/%s", mmsType, token),
RawQuery: q.Encode(),
}
var req *http.Request
req, err = http.NewRequestWithContext(ctx, http.MethodPost, uploadURL.String(), bytes.NewReader(dataToUpload))
if err != nil {
err = fmt.Errorf("failed to prepare request: %w", err)
return
}
req.Header.Set("Origin", socket.Origin)
req.Header.Set("Referer", socket.Origin+"/")
var httpResp *http.Response
httpResp, err = http.DefaultClient.Do(req)
if err != nil {
err = fmt.Errorf("failed to execute request: %w", err)
} else if httpResp.StatusCode != http.StatusOK {
err = fmt.Errorf("upload failed with status code %d", httpResp.StatusCode)
} else if err = json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
err = fmt.Errorf("failed to parse upload response: %w", err)
}
if httpResp != nil {
_ = httpResp.Body.Close()
}
return
}

368
vendor/go.mau.fi/whatsmeow/user.go vendored Normal file
View File

@ -0,0 +1,368 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"errors"
"fmt"
"strings"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
const BusinessMessageLinkPrefix = "https://wa.me/message/"
const BusinessMessageLinkDirectPrefix = "https://api.whatsapp.com/message/"
// ResolveBusinessMessageLink resolves a business message short link and returns the target JID, business name and
// text to prefill in the input field (if any).
//
// The links look like https://wa.me/message/<code> or https://api.whatsapp.com/message/<code>. You can either provide
// the full link, or just the <code> part.
func (cli *Client) ResolveBusinessMessageLink(code string) (*types.BusinessMessageLinkTarget, error) {
code = strings.TrimPrefix(code, BusinessMessageLinkPrefix)
code = strings.TrimPrefix(code, BusinessMessageLinkDirectPrefix)
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:qr",
Type: "get",
// WhatsApp android doesn't seem to have a "to" field for this one at all, not sure why but it works
Content: []waBinary.Node{{
Tag: "qr",
Attrs: waBinary.Attrs{
"code": code,
},
}},
})
if errors.Is(err, ErrIQNotFound) {
return nil, wrapIQError(ErrBusinessMessageLinkNotFound, err)
} else if err != nil {
return nil, err
}
qrChild, ok := resp.GetOptionalChildByTag("qr")
if !ok {
return nil, &ElementMissingError{Tag: "qr", In: "response to business message link query"}
}
var target types.BusinessMessageLinkTarget
ag := qrChild.AttrGetter()
target.JID = ag.JID("jid")
target.PushName = ag.String("notify")
messageChild, ok := qrChild.GetOptionalChildByTag("message")
if ok {
messageBytes, _ := messageChild.Content.([]byte)
target.Message = string(messageBytes)
}
businessChild, ok := qrChild.GetOptionalChildByTag("business")
if ok {
bag := businessChild.AttrGetter()
target.IsSigned = bag.OptionalBool("is_signed")
target.VerifiedName = bag.OptionalString("verified_name")
target.VerifiedLevel = bag.OptionalString("verified_level")
}
return &target, ag.Error()
}
// IsOnWhatsApp checks if the given phone numbers are registered on WhatsApp.
// The phone numbers should be in international format, including the `+` prefix.
func (cli *Client) IsOnWhatsApp(phones []string) ([]types.IsOnWhatsAppResponse, error) {
jids := make([]types.JID, len(phones))
for i := range jids {
jids[i] = types.NewJID(phones[i], types.LegacyUserServer)
}
list, err := cli.usync(jids, "query", "interactive", []waBinary.Node{
{Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
{Tag: "contact"},
})
if err != nil {
return nil, err
}
output := make([]types.IsOnWhatsAppResponse, 0, len(jids))
querySuffix := "@" + types.LegacyUserServer
for _, child := range list.GetChildren() {
jid, jidOK := child.Attrs["jid"].(types.JID)
if child.Tag != "user" || !jidOK {
continue
}
var info types.IsOnWhatsAppResponse
info.JID = jid
info.VerifiedName, err = parseVerifiedName(child.GetChildByTag("business"))
if err != nil {
cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
}
contactNode := child.GetChildByTag("contact")
info.IsIn = contactNode.AttrGetter().String("type") == "in"
contactQuery, _ := contactNode.Content.([]byte)
info.Query = strings.TrimSuffix(string(contactQuery), querySuffix)
output = append(output, info)
}
return output, nil
}
// GetUserInfo gets basic user info (avatar, status, verified business name, device list).
func (cli *Client) GetUserInfo(jids []types.JID) (map[types.JID]types.UserInfo, error) {
list, err := cli.usync(jids, "full", "background", []waBinary.Node{
{Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
{Tag: "status"},
{Tag: "picture"},
{Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
})
if err != nil {
return nil, err
}
respData := make(map[types.JID]types.UserInfo, len(jids))
for _, child := range list.GetChildren() {
jid, jidOK := child.Attrs["jid"].(types.JID)
if child.Tag != "user" || !jidOK {
continue
}
verifiedName, err := parseVerifiedName(child.GetChildByTag("business"))
if err != nil {
cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
}
status, _ := child.GetChildByTag("status").Content.([]byte)
pictureID, _ := child.GetChildByTag("picture").Attrs["id"].(string)
devices := parseDeviceList(jid.User, child.GetChildByTag("devices"))
respData[jid] = types.UserInfo{
VerifiedName: verifiedName,
Status: string(status),
PictureID: pictureID,
Devices: devices,
}
if verifiedName != nil {
cli.updateBusinessName(jid, verifiedName.Details.GetVerifiedName())
}
}
return respData, nil
}
// GetUserDevices gets the list of devices that the given user has. The input should be a list of
// regular JIDs, and the output will be a list of AD JIDs. The local device will not be included in
// the output even if the user's JID is included in the input. All other devices will be included.
func (cli *Client) GetUserDevices(jids []types.JID) ([]types.JID, error) {
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
var devices, jidsToSync []types.JID
for _, jid := range jids {
cached, ok := cli.userDevicesCache[jid]
if ok && len(cached) > 0 {
devices = append(devices, cached...)
} else {
jidsToSync = append(jidsToSync, jid)
}
}
if len(jidsToSync) == 0 {
return devices, nil
}
list, err := cli.usync(jidsToSync, "query", "message", []waBinary.Node{
{Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
})
if err != nil {
return nil, err
}
for _, user := range list.GetChildren() {
jid, jidOK := user.Attrs["jid"].(types.JID)
if user.Tag != "user" || !jidOK {
continue
}
userDevices := parseDeviceList(jid.User, user.GetChildByTag("devices"))
cli.userDevicesCache[jid] = userDevices
devices = append(devices, userDevices...)
}
return devices, nil
}
// GetProfilePictureInfo gets the URL where you can download a WhatsApp user's profile picture or group's photo.
// If the user or group doesn't have a profile picture, this returns nil with no error.
func (cli *Client) GetProfilePictureInfo(jid types.JID, preview bool) (*types.ProfilePictureInfo, error) {
attrs := waBinary.Attrs{
"query": "url",
}
if preview {
attrs["type"] = "preview"
} else {
attrs["type"] = "image"
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:profile:picture",
Type: "get",
To: jid,
Content: []waBinary.Node{{
Tag: "picture",
Attrs: attrs,
}},
})
if errors.Is(err, ErrIQNotAuthorized) {
return nil, wrapIQError(ErrProfilePictureUnauthorized, err)
} else if errors.Is(err, ErrIQNotFound) {
return nil, nil
} else if err != nil {
return nil, err
}
picture, ok := resp.GetOptionalChildByTag("picture")
if !ok {
return nil, &ElementMissingError{Tag: "picture", In: "response to profile picture query"}
}
var info types.ProfilePictureInfo
ag := picture.AttrGetter()
info.ID = ag.String("id")
info.URL = ag.String("url")
info.Type = ag.String("type")
info.DirectPath = ag.String("direct_path")
if !ag.OK() {
return &info, ag.Error()
}
return &info, nil
}
func (cli *Client) handleHistoricalPushNames(names []*waProto.Pushname) {
if cli.Store.Contacts == nil {
return
}
for _, user := range names {
if user.GetPushname() == "-" {
continue
}
var changed bool
if jid, err := types.ParseJID(user.GetId()); err != nil {
cli.Log.Warnf("Failed to parse user ID '%s' in push name history sync: %v", user.GetId(), err)
} else if changed, _, err = cli.Store.Contacts.PutPushName(jid, user.GetPushname()); err != nil {
cli.Log.Warnf("Failed to store push name of %s from history sync: %v", err)
} else if changed {
cli.Log.Debugf("Got push name %s for %s in history sync", user.GetPushname(), jid)
}
}
}
func (cli *Client) updatePushName(user types.JID, messageInfo *types.MessageInfo, name string) {
if cli.Store.Contacts == nil {
return
}
user = user.ToNonAD()
changed, previousName, err := cli.Store.Contacts.PutPushName(user, name)
if err != nil {
cli.Log.Errorf("Failed to save push name of %s in device store: %v", user, err)
} else if changed {
cli.Log.Debugf("Push name of %s changed from %s to %s, dispatching event", user, previousName, name)
cli.dispatchEvent(&events.PushName{
JID: user,
Message: messageInfo,
OldPushName: previousName,
NewPushName: name,
})
}
}
func (cli *Client) updateBusinessName(user types.JID, name string) {
if cli.Store.Contacts == nil {
return
}
err := cli.Store.Contacts.PutBusinessName(user, name)
if err != nil {
cli.Log.Errorf("Failed to save business name of %s in device store: %v", user, err)
}
}
func parseVerifiedName(businessNode waBinary.Node) (*types.VerifiedName, error) {
if businessNode.Tag != "business" {
return nil, nil
}
verifiedNameNode, ok := businessNode.GetOptionalChildByTag("verified_name")
if !ok {
return nil, nil
}
rawCert, ok := verifiedNameNode.Content.([]byte)
if !ok {
return nil, nil
}
var cert waProto.VerifiedNameCertificate
err := proto.Unmarshal(rawCert, &cert)
if err != nil {
return nil, err
}
var certDetails waProto.VerifiedNameDetails
err = proto.Unmarshal(cert.GetDetails(), &certDetails)
if err != nil {
return nil, err
}
return &types.VerifiedName{
Certificate: &cert,
Details: &certDetails,
}, nil
}
func parseDeviceList(user string, deviceNode waBinary.Node) []types.JID {
deviceList := deviceNode.GetChildByTag("device-list")
if deviceNode.Tag != "devices" || deviceList.Tag != "device-list" {
return nil
}
children := deviceList.GetChildren()
devices := make([]types.JID, 0, len(children))
for _, device := range children {
deviceID, ok := device.AttrGetter().GetInt64("id", true)
if device.Tag != "device" || !ok {
continue
}
devices = append(devices, types.NewADJID(user, 0, byte(deviceID)))
}
return devices
}
func (cli *Client) usync(jids []types.JID, mode, context string, query []waBinary.Node) (*waBinary.Node, error) {
userList := make([]waBinary.Node, len(jids))
for i, jid := range jids {
userList[i].Tag = "user"
if jid.AD {
jid.AD = false
}
switch jid.Server {
case types.LegacyUserServer:
userList[i].Content = []waBinary.Node{{
Tag: "contact",
Content: jid.String(),
}}
case types.DefaultUserServer:
userList[i].Attrs = waBinary.Attrs{"jid": jid}
default:
return nil, fmt.Errorf("unknown user server '%s'", jid.Server)
}
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "usync",
Type: "get",
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "usync",
Attrs: waBinary.Attrs{
"sid": cli.generateRequestID(),
"mode": mode,
"last": "true",
"index": "0",
"context": context,
},
Content: []waBinary.Node{
{Tag: "query", Content: query},
{Tag: "list", Content: userList},
},
}},
})
if err != nil {
return nil, fmt.Errorf("failed to send usync query: %w", err)
} else if list, ok := resp.GetOptionalChildByTag("usync", "list"); !ok {
return nil, &ElementMissingError{Tag: "list", In: "response to usync query"}
} else {
return &list, err
}
}

View File

@ -0,0 +1,101 @@
/*
CBC describes a block cipher mode. In cryptography, a block cipher mode of operation is an algorithm that uses a
block cipher to provide an information service such as confidentiality or authenticity. A block cipher by itself
is only suitable for the secure cryptographic transformation (encryption or decryption) of one fixed-length group of
bits called a block. A mode of operation describes how to repeatedly apply a cipher's single-block operation to
securely transform amounts of data larger than a block.
This package simplifies the usage of AES-256-CBC.
*/
package cbcutil
/*
Some code is provided by the GitHub user locked (github.com/locked):
https://gist.github.com/locked/b066aa1ddeb2b28e855e
Thanks!
*/
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
/*
Decrypt is a function that decrypts a given cipher text with a provided key and initialization vector(iv).
*/
func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("ciphertext is shorter then block size: %d / %d", len(ciphertext), aes.BlockSize)
}
if iv == nil {
iv = ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
}
cbc := cipher.NewCBCDecrypter(block, iv)
cbc.CryptBlocks(ciphertext, ciphertext)
return unpad(ciphertext)
}
/*
Encrypt is a function that encrypts plaintext with a given key and an optional initialization vector(iv).
*/
func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
plaintext = pad(plaintext, aes.BlockSize)
if len(plaintext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("plaintext is not a multiple of the block size: %d / %d", len(plaintext), aes.BlockSize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
var ciphertext []byte
if iv == nil {
ciphertext = make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
} else {
ciphertext = make([]byte, len(plaintext))
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(ciphertext, plaintext)
}
return ciphertext, nil
}
func pad(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func unpad(src []byte) ([]byte, error) {
length := len(src)
padLen := int(src[length-1])
if padLen > length {
return nil, fmt.Errorf("padding is greater then the length: %d / %d", padLen, length)
}
return src[:(length - padLen)], nil
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package hkdfutil contains a simple wrapper for golang.org/x/crypto/hkdf that reads a specified number of bytes.
package hkdfutil
import (
"crypto/sha256"
"fmt"
"golang.org/x/crypto/hkdf"
)
func SHA256(key, salt, info []byte, length uint8) []byte {
data := make([]byte, length)
h := hkdf.New(sha256.New, key, salt, info)
n, err := h.Read(data)
if err != nil {
// Length is limited to 255 by being uint8, so these errors can't actually happen
panic(fmt.Errorf("failed to expand key: %w", err))
} else if uint8(n) != length {
panic(fmt.Errorf("didn't read enough bytes (got %d, wanted %d)", n, length))
}
return data
}

View File

@ -0,0 +1,75 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package keys contains a utility struct for elliptic curve keypairs.
package keys
import (
"crypto/rand"
"fmt"
"go.mau.fi/libsignal/ecc"
"golang.org/x/crypto/curve25519"
)
type KeyPair struct {
Pub *[32]byte
Priv *[32]byte
}
var _ ecc.ECPublicKeyable
func NewKeyPairFromPrivateKey(priv [32]byte) *KeyPair {
var kp KeyPair
kp.Priv = &priv
var pub [32]byte
curve25519.ScalarBaseMult(&pub, kp.Priv)
kp.Pub = &pub
return &kp
}
func NewKeyPair() *KeyPair {
var priv [32]byte
_, err := rand.Read(priv[:])
if err != nil {
panic(fmt.Errorf("failed to generate curve25519 private key: %w", err))
}
priv[0] &= 248
priv[31] &= 127
priv[31] |= 64
return NewKeyPairFromPrivateKey(priv)
}
func (kp *KeyPair) CreateSignedPreKey(keyID uint32) *PreKey {
newKey := NewPreKey(keyID)
newKey.Signature = kp.Sign(&newKey.KeyPair)
return newKey
}
func (kp *KeyPair) Sign(keyToSign *KeyPair) *[64]byte {
pubKeyForSignature := make([]byte, 33)
pubKeyForSignature[0] = ecc.DjbType
copy(pubKeyForSignature[1:], keyToSign.Pub[:])
signature := ecc.CalculateSignature(ecc.NewDjbECPrivateKey(*kp.Priv), pubKeyForSignature)
return &signature
}
type PreKey struct {
KeyPair
KeyID uint32
Signature *[64]byte
}
func NewPreKey(keyID uint32) *PreKey {
return &PreKey{
KeyPair: *NewKeyPair(),
KeyID: keyID,
}
}

View File

@ -0,0 +1,83 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package waLog contains a simple logger interface used by the other whatsmeow packages.
package waLog
import (
"fmt"
"strings"
"time"
)
// Logger is a simple logger interface that can have subloggers for specific areas.
type Logger interface {
Warnf(msg string, args ...interface{})
Errorf(msg string, args ...interface{})
Infof(msg string, args ...interface{})
Debugf(msg string, args ...interface{})
Sub(module string) Logger
}
type noopLogger struct{}
func (n *noopLogger) Errorf(_ string, _ ...interface{}) {}
func (n *noopLogger) Warnf(_ string, _ ...interface{}) {}
func (n *noopLogger) Infof(_ string, _ ...interface{}) {}
func (n *noopLogger) Debugf(_ string, _ ...interface{}) {}
func (n *noopLogger) Sub(_ string) Logger { return n }
// Noop is a no-op Logger implementation that silently drops everything.
var Noop Logger = &noopLogger{}
type stdoutLogger struct {
mod string
color bool
min int
}
var colors = map[string]string{
"INFO": "\033[36m",
"WARN": "\033[33m",
"ERROR": "\033[31m",
}
var levelToInt = map[string]int{
"": -1,
"DEBUG": 0,
"INFO": 1,
"WARN": 2,
"ERROR": 3,
}
func (s *stdoutLogger) outputf(level, msg string, args ...interface{}) {
if levelToInt[level] < s.min {
return
}
var colorStart, colorReset string
if s.color {
colorStart = colors[level]
colorReset = "\033[0m"
}
fmt.Printf("%s%s [%s %s] %s%s\n", time.Now().Format("15:04:05.000"), colorStart, s.mod, level, fmt.Sprintf(msg, args...), colorReset)
}
func (s *stdoutLogger) Errorf(msg string, args ...interface{}) { s.outputf("ERROR", msg, args...) }
func (s *stdoutLogger) Warnf(msg string, args ...interface{}) { s.outputf("WARN", msg, args...) }
func (s *stdoutLogger) Infof(msg string, args ...interface{}) { s.outputf("INFO", msg, args...) }
func (s *stdoutLogger) Debugf(msg string, args ...interface{}) { s.outputf("DEBUG", msg, args...) }
func (s *stdoutLogger) Sub(mod string) Logger {
return &stdoutLogger{mod: fmt.Sprintf("%s/%s", s.mod, mod), color: s.color, min: s.min}
}
// Stdout is a simple Logger implementation that outputs to stdout. The module name given is included in log lines.
//
// minLevel specifies the minimum log level to output. An empty string will output all logs.
//
// If color is true, then info, warn and error logs will be colored cyan, yellow and red respectively using ANSI color escape codes.
func Stdout(module string, minLevel string, color bool) Logger {
return &stdoutLogger{mod: module, color: color, min: levelToInt[strings.ToUpper(minLevel)]}
}