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:
374
vendor/go.mau.fi/whatsmeow/LICENSE
vendored
Normal file
374
vendor/go.mau.fi/whatsmeow/LICENSE
vendored
Normal 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
39
vendor/go.mau.fi/whatsmeow/README.md
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# whatsmeow
|
||||
[](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
193
vendor/go.mau.fi/whatsmeow/appstate.go
vendored
Normal 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)
|
||||
}
|
310
vendor/go.mau.fi/whatsmeow/appstate/decode.go
vendored
Normal file
310
vendor/go.mau.fi/whatsmeow/appstate/decode.go
vendored
Normal 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
|
||||
}
|
19
vendor/go.mau.fi/whatsmeow/appstate/errors.go
vendored
Normal file
19
vendor/go.mau.fi/whatsmeow/appstate/errors.go
vendored
Normal 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")
|
||||
)
|
96
vendor/go.mau.fi/whatsmeow/appstate/hash.go
vendored
Normal file
96
vendor/go.mau.fi/whatsmeow/appstate/hash.go
vendored
Normal 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]
|
||||
}
|
85
vendor/go.mau.fi/whatsmeow/appstate/keys.go
vendored
Normal file
85
vendor/go.mau.fi/whatsmeow/appstate/keys.go
vendored
Normal 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
|
||||
}
|
58
vendor/go.mau.fi/whatsmeow/appstate/lthash/lthash.go
vendored
Normal file
58
vendor/go.mau.fi/whatsmeow/appstate/lthash/lthash.go
vendored
Normal 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
|
||||
}
|
177
vendor/go.mau.fi/whatsmeow/binary/attrs.go
vendored
Normal file
177
vendor/go.mau.fi/whatsmeow/binary/attrs.go
vendored
Normal 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))
|
||||
}
|
353
vendor/go.mau.fi/whatsmeow/binary/decoder.go
vendored
Normal file
353
vendor/go.mau.fi/whatsmeow/binary/decoder.go
vendored
Normal 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
|
||||
}
|
293
vendor/go.mau.fi/whatsmeow/binary/encoder.go
vendored
Normal file
293
vendor/go.mau.fi/whatsmeow/binary/encoder.go
vendored
Normal 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)))
|
||||
}
|
||||
}
|
12
vendor/go.mau.fi/whatsmeow/binary/errors.go
vendored
Normal file
12
vendor/go.mau.fi/whatsmeow/binary/errors.go
vendored
Normal 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")
|
||||
)
|
83
vendor/go.mau.fi/whatsmeow/binary/node.go
vendored
Normal file
83
vendor/go.mau.fi/whatsmeow/binary/node.go
vendored
Normal 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
|
||||
}
|
21520
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go
vendored
Normal file
21520
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw
vendored
Normal file
BIN
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw
vendored
Normal file
Binary file not shown.
2011
vendor/go.mau.fi/whatsmeow/binary/proto/def.proto
vendored
Normal file
2011
vendor/go.mau.fi/whatsmeow/binary/proto/def.proto
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendor/go.mau.fi/whatsmeow/binary/proto/doc.go
vendored
Normal file
2
vendor/go.mau.fi/whatsmeow/binary/proto/doc.go
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
// Package proto contains the compiled protobuf structs from WhatsApp's protobuf schema.
|
||||
package proto
|
88
vendor/go.mau.fi/whatsmeow/binary/token/token.go
vendored
Normal file
88
vendor/go.mau.fi/whatsmeow/binary/token/token.go
vendored
Normal file
File diff suppressed because one or more lines are too long
31
vendor/go.mau.fi/whatsmeow/binary/unpack.go
vendored
Normal file
31
vendor/go.mau.fi/whatsmeow/binary/unpack.go
vendored
Normal 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
108
vendor/go.mau.fi/whatsmeow/binary/xml.go
vendored
Normal 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
73
vendor/go.mau.fi/whatsmeow/call.go
vendored
Normal 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
459
vendor/go.mau.fi/whatsmeow/client.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
141
vendor/go.mau.fi/whatsmeow/connectionevents.go
vendored
Normal file
141
vendor/go.mau.fi/whatsmeow/connectionevents.go
vendored
Normal 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
213
vendor/go.mau.fi/whatsmeow/download.go
vendored
Normal 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
157
vendor/go.mau.fi/whatsmeow/errors.go
vendored
Normal 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
566
vendor/go.mau.fi/whatsmeow/group.go
vendored
Normal 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
131
vendor/go.mau.fi/whatsmeow/handshake.go
vendored
Normal 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
62
vendor/go.mau.fi/whatsmeow/keepalive.go
vendored
Normal 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
93
vendor/go.mau.fi/whatsmeow/mediaconn.go
vendored
Normal 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
383
vendor/go.mau.fi/whatsmeow/message.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
205
vendor/go.mau.fi/whatsmeow/notification.go
vendored
Normal file
205
vendor/go.mau.fi/whatsmeow/notification.go
vendored
Normal 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
244
vendor/go.mau.fi/whatsmeow/pair.go
vendored
Normal 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
235
vendor/go.mau.fi/whatsmeow/prekeys.go
vendored
Normal 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
101
vendor/go.mau.fi/whatsmeow/presence.go
vendored
Normal 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)}},
|
||||
})
|
||||
}
|
93
vendor/go.mau.fi/whatsmeow/privacysettings.go
vendored
Normal file
93
vendor/go.mau.fi/whatsmeow/privacysettings.go
vendored
Normal 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
168
vendor/go.mau.fi/whatsmeow/qrchan.go
vendored
Normal 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
148
vendor/go.mau.fi/whatsmeow/receipt.go
vendored
Normal 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
141
vendor/go.mau.fi/whatsmeow/request.go
vendored
Normal 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
264
vendor/go.mau.fi/whatsmeow/retry.go
vendored
Normal 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
357
vendor/go.mau.fi/whatsmeow/send.go
vendored
Normal 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
|
||||
}
|
40
vendor/go.mau.fi/whatsmeow/socket/constants.go
vendored
Normal file
40
vendor/go.mau.fi/whatsmeow/socket/constants.go
vendored
Normal 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")
|
||||
)
|
228
vendor/go.mau.fi/whatsmeow/socket/framesocket.go
vendored
Normal file
228
vendor/go.mau.fi/whatsmeow/socket/framesocket.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
135
vendor/go.mau.fi/whatsmeow/socket/noisehandshake.go
vendored
Normal file
135
vendor/go.mau.fi/whatsmeow/socket/noisehandshake.go
vendored
Normal 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
|
||||
}
|
104
vendor/go.mau.fi/whatsmeow/socket/noisesocket.go
vendored
Normal file
104
vendor/go.mau.fi/whatsmeow/socket/noisesocket.go
vendored
Normal 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()
|
||||
}
|
118
vendor/go.mau.fi/whatsmeow/store/clientpayload.go
vendored
Normal file
118
vendor/go.mau.fi/whatsmeow/store/clientpayload.go
vendored
Normal 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()
|
||||
}
|
||||
}
|
168
vendor/go.mau.fi/whatsmeow/store/signal.go
vendored
Normal file
168
vendor/go.mau.fi/whatsmeow/store/signal.go
vendored
Normal 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
|
||||
}
|
245
vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go
vendored
Normal file
245
vendor/go.mau.fi/whatsmeow/store/sqlstore/container.go
vendored
Normal 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
|
||||
}
|
610
vendor/go.mau.fi/whatsmeow/store/sqlstore/store.go
vendored
Normal file
610
vendor/go.mau.fi/whatsmeow/store/sqlstore/store.go
vendored
Normal 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
|
||||
}
|
214
vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go
vendored
Normal file
214
vendor/go.mau.fi/whatsmeow/store/sqlstore/upgrade.go
vendored
Normal 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
|
||||
}
|
132
vendor/go.mau.fi/whatsmeow/store/store.go
vendored
Normal file
132
vendor/go.mau.fi/whatsmeow/store/store.go
vendored
Normal 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
|
||||
}
|
21
vendor/go.mau.fi/whatsmeow/types/call.go
vendored
Normal file
21
vendor/go.mau.fi/whatsmeow/types/call.go
vendored
Normal 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
|
||||
}
|
103
vendor/go.mau.fi/whatsmeow/types/events/appstate.go
vendored
Normal file
103
vendor/go.mau.fi/whatsmeow/types/events/appstate.go
vendored
Normal 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
|
||||
}
|
57
vendor/go.mau.fi/whatsmeow/types/events/call.go
vendored
Normal file
57
vendor/go.mau.fi/whatsmeow/types/events/call.go
vendored
Normal 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
|
||||
}
|
263
vendor/go.mau.fi/whatsmeow/types/events/events.go
vendored
Normal file
263
vendor/go.mau.fi/whatsmeow/types/events/events.go
vendored
Normal 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
|
||||
}
|
67
vendor/go.mau.fi/whatsmeow/types/group.go
vendored
Normal file
67
vendor/go.mau.fi/whatsmeow/types/group.go
vendored
Normal 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
193
vendor/go.mau.fi/whatsmeow/types/jid.go
vendored
Normal 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
|
||||
}
|
58
vendor/go.mau.fi/whatsmeow/types/message.go
vendored
Normal file
58
vendor/go.mau.fi/whatsmeow/types/message.go
vendored
Normal 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()
|
||||
}
|
||||
}
|
22
vendor/go.mau.fi/whatsmeow/types/presence.go
vendored
Normal file
22
vendor/go.mau.fi/whatsmeow/types/presence.go
vendored
Normal 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"
|
||||
)
|
96
vendor/go.mau.fi/whatsmeow/types/user.go
vendored
Normal file
96
vendor/go.mau.fi/whatsmeow/types/user.go
vendored
Normal 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
107
vendor/go.mau.fi/whatsmeow/upload.go
vendored
Normal 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
368
vendor/go.mau.fi/whatsmeow/user.go
vendored
Normal 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
|
||||
}
|
||||
}
|
101
vendor/go.mau.fi/whatsmeow/util/cbcutil/cbc.go
vendored
Normal file
101
vendor/go.mau.fi/whatsmeow/util/cbcutil/cbc.go
vendored
Normal 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
|
||||
}
|
28
vendor/go.mau.fi/whatsmeow/util/hkdfutil/hkdf.go
vendored
Normal file
28
vendor/go.mau.fi/whatsmeow/util/hkdfutil/hkdf.go
vendored
Normal 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
|
||||
}
|
75
vendor/go.mau.fi/whatsmeow/util/keys/keypair.go
vendored
Normal file
75
vendor/go.mau.fi/whatsmeow/util/keys/keypair.go
vendored
Normal 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,
|
||||
}
|
||||
}
|
83
vendor/go.mau.fi/whatsmeow/util/log/log.go
vendored
Normal file
83
vendor/go.mau.fi/whatsmeow/util/log/log.go
vendored
Normal 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)]}
|
||||
}
|
Reference in New Issue
Block a user