forked from lug/matterbridge
Update vendor (whatsapp)
This commit is contained in:
12
vendor/go.mau.fi/whatsmeow/README.md
vendored
12
vendor/go.mau.fi/whatsmeow/README.md
vendored
@@ -1,21 +1,13 @@
|
||||
# whatsmeow
|
||||
[](https://godocs.io/go.mau.fi/whatsmeow)
|
||||
[](https://pkg.go.dev/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.
|
||||
The [godoc](https://pkg.go.dev/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.
|
||||
|
||||
39
vendor/go.mau.fi/whatsmeow/appstate.go
vendored
39
vendor/go.mau.fi/whatsmeow/appstate.go
vendored
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021 Tulir Asokan
|
||||
// Copyright (c) 2022 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
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -54,7 +55,18 @@ func (cli *Client) FetchAppState(name appstate.WAPatchName, fullSync, onlyIfNotS
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode app state %s patches: %w", name, err)
|
||||
}
|
||||
wasFullSync := state.Version == 0 && patches.Snapshot != nil
|
||||
state = newState
|
||||
if name == appstate.WAPatchCriticalUnblockLow && wasFullSync && !cli.EmitAppStateEventsOnFullSync {
|
||||
var contacts []store.ContactEntry
|
||||
mutations, contacts = cli.filterContacts(mutations)
|
||||
cli.Log.Debugf("Mass inserting app state snapshot with %d contacts into the store", len(contacts))
|
||||
err = cli.Store.Contacts.PutAllContactNames(contacts)
|
||||
if err != nil {
|
||||
// This is a fairly serious failure, so just abort the whole thing
|
||||
return fmt.Errorf("failed to update contact store with data from snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
for _, mutation := range mutations {
|
||||
cli.dispatchAppState(mutation, !fullSync || cli.EmitAppStateEventsOnFullSync)
|
||||
}
|
||||
@@ -68,6 +80,25 @@ func (cli *Client) FetchAppState(name appstate.WAPatchName, fullSync, onlyIfNotS
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *Client) filterContacts(mutations []appstate.Mutation) ([]appstate.Mutation, []store.ContactEntry) {
|
||||
filteredMutations := mutations[:0]
|
||||
contacts := make([]store.ContactEntry, 0, len(mutations))
|
||||
for _, mutation := range mutations {
|
||||
if mutation.Index[0] == "contact" && len(mutation.Index) > 1 {
|
||||
jid, _ := types.ParseJID(mutation.Index[1])
|
||||
act := mutation.Action.GetContactAction()
|
||||
contacts = append(contacts, store.ContactEntry{
|
||||
JID: jid,
|
||||
FirstName: act.GetFirstName(),
|
||||
FullName: act.GetFullName(),
|
||||
})
|
||||
} else {
|
||||
filteredMutations = append(filteredMutations, mutation)
|
||||
}
|
||||
}
|
||||
return filteredMutations, contacts
|
||||
}
|
||||
|
||||
func (cli *Client) dispatchAppState(mutation appstate.Mutation, dispatchEvts bool) {
|
||||
if mutation.Operation != waProto.SyncdMutation_SET {
|
||||
return
|
||||
@@ -144,6 +175,12 @@ func (cli *Client) dispatchAppState(mutation appstate.Mutation, dispatchEvts boo
|
||||
evt.SenderJID, _ = types.ParseJID(mutation.Index[4])
|
||||
}
|
||||
eventToDispatch = &evt
|
||||
case "markChatAsRead":
|
||||
eventToDispatch = &events.MarkChatAsRead{
|
||||
JID: jid,
|
||||
Timestamp: ts,
|
||||
Action: mutation.Action.GetMarkChatAsReadAction(),
|
||||
}
|
||||
case "setting_pushName":
|
||||
eventToDispatch = &events.PushNameSetting{Timestamp: ts, Action: mutation.Action.GetPushNameSetting()}
|
||||
cli.Store.PushName = mutation.Action.GetPushNameSetting().GetName()
|
||||
|
||||
35254
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go
vendored
35254
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.go
vendored
File diff suppressed because it is too large
Load Diff
BIN
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw
vendored
BIN
vendor/go.mau.fi/whatsmeow/binary/proto/def.pb.raw
vendored
Binary file not shown.
3247
vendor/go.mau.fi/whatsmeow/binary/proto/def.proto
vendored
3247
vendor/go.mau.fi/whatsmeow/binary/proto/def.proto
vendored
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,10 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// DictVersion is the version number of the token lists above.
|
||||
// It's sent when connecting to the websocket so the server knows which tokens are supported.
|
||||
const DictVersion = 2
|
||||
|
||||
type doubleByteTokenIndex struct {
|
||||
dictionary byte
|
||||
index byte
|
||||
|
||||
95
vendor/go.mau.fi/whatsmeow/client.go
vendored
95
vendor/go.mau.fi/whatsmeow/client.go
vendored
@@ -13,6 +13,8 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -56,6 +58,8 @@ type Client struct {
|
||||
LastSuccessfulConnect time.Time
|
||||
AutoReconnectErrors int
|
||||
|
||||
sendActiveReceipts uint32
|
||||
|
||||
// EmitAppStateEventsOnFullSync can be set to true if you want to get app state events emitted
|
||||
// even when re-syncing the whole state.
|
||||
EmitAppStateEventsOnFullSync bool
|
||||
@@ -100,6 +104,9 @@ type Client struct {
|
||||
|
||||
uniqueID string
|
||||
idCounter uint32
|
||||
|
||||
proxy socket.Proxy
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// Size of buffer for the channel that all incoming XML nodes go through.
|
||||
@@ -128,6 +135,10 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
|
||||
randomBytes := make([]byte, 2)
|
||||
_, _ = rand.Read(randomBytes)
|
||||
cli := &Client{
|
||||
http: &http.Client{
|
||||
Transport: (http.DefaultTransport.(*http.Transport)).Clone(),
|
||||
},
|
||||
proxy: http.ProxyFromEnvironment,
|
||||
Store: deviceStore,
|
||||
Log: log,
|
||||
recvLog: log.Sub("Recv"),
|
||||
@@ -159,10 +170,46 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
|
||||
"stream:error": cli.handleStreamError,
|
||||
"iq": cli.handleIQ,
|
||||
"ib": cli.handleIB,
|
||||
// Apparently there's also an <error> node which can have a code=479 and means "Invalid stanza sent (smax-invalid)"
|
||||
}
|
||||
return cli
|
||||
}
|
||||
|
||||
// SetProxyAddress is a helper method that parses a URL string and calls SetProxy.
|
||||
//
|
||||
// Returns an error if url.Parse fails to parse the given address.
|
||||
func (cli *Client) SetProxyAddress(addr string) error {
|
||||
parsed, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.SetProxy(http.ProxyURL(parsed))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProxy sets the proxy to use for WhatsApp web websocket connections and media uploads/downloads.
|
||||
//
|
||||
// Must be called before Connect() to take effect in the websocket connection.
|
||||
// If you want to change the proxy after connecting, you must call Disconnect() and then Connect() again manually.
|
||||
//
|
||||
// By default, the client will find the proxy from the https_proxy environment variable like Go's net/http does.
|
||||
//
|
||||
// To disable reading proxy info from environment variables, explicitly set the proxy to nil:
|
||||
// cli.SetProxy(nil)
|
||||
//
|
||||
// To use a different proxy for the websocket and media, pass a function that checks the request path or headers:
|
||||
// cli.SetProxy(func(r *http.Request) (*url.URL, error) {
|
||||
// if r.URL.Host == "web.whatsapp.com" && r.URL.Path == "/ws/chat" {
|
||||
// return websocketProxyURL, nil
|
||||
// } else {
|
||||
// return mediaProxyURL, nil
|
||||
// }
|
||||
// })
|
||||
func (cli *Client) SetProxy(proxy socket.Proxy) {
|
||||
cli.proxy = proxy
|
||||
cli.http.Transport.(*http.Transport).Proxy = proxy
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -177,7 +224,7 @@ func (cli *Client) Connect() error {
|
||||
}
|
||||
|
||||
cli.resetExpectedDisconnect()
|
||||
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), socket.WAConnHeader)
|
||||
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), socket.WAConnHeader, cli.proxy)
|
||||
if err := fs.Connect(); err != nil {
|
||||
fs.Close(0)
|
||||
return err
|
||||
@@ -313,29 +360,29 @@ func (cli *Client) Logout() error {
|
||||
//
|
||||
// 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!")
|
||||
// }
|
||||
// }
|
||||
// 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
|
||||
// }
|
||||
// type MyClient struct {
|
||||
// WAClient *whatsmeow.Client
|
||||
// eventHandlerID uint32
|
||||
// }
|
||||
//
|
||||
// func (mycli *MyClient) register() {
|
||||
// mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler)
|
||||
// }
|
||||
// func (mycli *MyClient) register() {
|
||||
// mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler)
|
||||
// }
|
||||
//
|
||||
// func (mycli *MyClient) myEventHandler(evt interface{}) {
|
||||
// // Handle event and access mycli.WAClient
|
||||
// }
|
||||
// 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()
|
||||
@@ -350,11 +397,11 @@ func (cli *Client) AddEventHandler(handler EventHandler) uint32 {
|
||||
// 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 (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()
|
||||
|
||||
29
vendor/go.mau.fi/whatsmeow/connectionevents.go
vendored
29
vendor/go.mau.fi/whatsmeow/connectionevents.go
vendored
@@ -33,7 +33,7 @@ func (cli *Client) handleStreamError(node *waBinary.Node) {
|
||||
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})
|
||||
go cli.dispatchEvent(&events.LoggedOut{OnConnect: false, Reason: events.ConnectFailureLoggedOut})
|
||||
err := cli.Store.Delete()
|
||||
if err != nil {
|
||||
cli.Log.Warnf("Failed to delete store after device_removed error: %v", err)
|
||||
@@ -77,17 +77,30 @@ func (cli *Client) handleIB(node *waBinary.Node) {
|
||||
|
||||
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})
|
||||
reason := events.ConnectFailureReason(ag.Int("reason"))
|
||||
cli.expectDisconnect()
|
||||
if reason.IsLoggedOut() {
|
||||
cli.Log.Infof("Got %s connect failure, sending LoggedOut event and deleting session", reason)
|
||||
go cli.dispatchEvent(&events.LoggedOut{OnConnect: true, Reason: reason})
|
||||
err := cli.Store.Delete()
|
||||
if err != nil {
|
||||
cli.Log.Warnf("Failed to delete store after 401 failure: %v", err)
|
||||
cli.Log.Warnf("Failed to delete store after %d failure: %v", int(reason), err)
|
||||
}
|
||||
} else if reason == events.ConnectFailureTempBanned {
|
||||
cli.Log.Warnf("Temporary ban connect failure: %s", node.XMLString())
|
||||
expiryTimeUnix := ag.Int64("expire")
|
||||
var expiryTime time.Time
|
||||
if expiryTimeUnix > 0 {
|
||||
expiryTime = time.Unix(expiryTimeUnix, 0)
|
||||
}
|
||||
go cli.dispatchEvent(&events.TemporaryBan{
|
||||
Code: events.TempBanReason(ag.Int("code")),
|
||||
Expire: expiryTime,
|
||||
})
|
||||
} else if reason == events.ConnectFailureClientOutdated {
|
||||
cli.Log.Errorf("Client outdated (405) connect failure")
|
||||
go cli.dispatchEvent(&events.ClientOutdated{})
|
||||
} else {
|
||||
cli.expectDisconnect()
|
||||
cli.Log.Warnf("Unknown connect failure: %s", node.XMLString())
|
||||
go cli.dispatchEvent(&events.ConnectFailure{Reason: reason, Raw: node})
|
||||
}
|
||||
|
||||
119
vendor/go.mau.fi/whatsmeow/download.go
vendored
119
vendor/go.mau.fi/whatsmeow/download.go
vendored
@@ -18,6 +18,7 @@ import (
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
"go.mau.fi/whatsmeow/socket"
|
||||
"go.mau.fi/whatsmeow/util/cbcutil"
|
||||
"go.mau.fi/whatsmeow/util/hkdfutil"
|
||||
)
|
||||
@@ -34,9 +35,14 @@ const (
|
||||
MediaDocument MediaType = "WhatsApp Document Keys"
|
||||
MediaHistory MediaType = "WhatsApp History Keys"
|
||||
MediaAppState MediaType = "WhatsApp App State Keys"
|
||||
|
||||
MediaLinkThumbnail MediaType = "WhatsApp Link Thumbnail Keys"
|
||||
)
|
||||
|
||||
// DownloadableMessage represents a protobuf message that contains attachment info.
|
||||
//
|
||||
// All of the downloadable messages inside a Message struct implement this interface
|
||||
// (ImageMessage, VideoMessage, AudioMessage, DocumentMessage, StickerMessage).
|
||||
type DownloadableMessage interface {
|
||||
proto.Message
|
||||
GetDirectPath() string
|
||||
@@ -45,15 +51,27 @@ type DownloadableMessage interface {
|
||||
GetFileEncSha256() []byte
|
||||
}
|
||||
|
||||
// DownloadableThumbnail represents a protobuf message that contains a thumbnail attachment.
|
||||
//
|
||||
// This is primarily meant for link preview thumbnails in ExtendedTextMessage.
|
||||
type DownloadableThumbnail interface {
|
||||
proto.Message
|
||||
GetThumbnailDirectPath() string
|
||||
GetThumbnailSha256() []byte
|
||||
GetThumbnailEncSha256() []byte
|
||||
GetMediaKey() []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)
|
||||
_ 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)
|
||||
_ DownloadableThumbnail = (*waProto.ExtendedTextMessage)(nil)
|
||||
)
|
||||
|
||||
type downloadableMessageWithLength interface {
|
||||
@@ -82,6 +100,10 @@ var classToMediaType = map[protoreflect.Name]MediaType{
|
||||
"ExternalBlobReference": MediaAppState,
|
||||
}
|
||||
|
||||
var classToThumbnailMediaType = map[protoreflect.Name]MediaType{
|
||||
"ExtendedTextMessage": MediaLinkThumbnail,
|
||||
}
|
||||
|
||||
var mediaTypeToMMSType = map[MediaType]string{
|
||||
MediaImage: "image",
|
||||
MediaAudio: "audio",
|
||||
@@ -89,17 +111,29 @@ var mediaTypeToMMSType = map[MediaType]string{
|
||||
MediaDocument: "document",
|
||||
MediaHistory: "md-msg-hist",
|
||||
MediaAppState: "md-app-state",
|
||||
|
||||
MediaLinkThumbnail: "thumbnail-link",
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
if msg == nil {
|
||||
return nil, ErrNothingDownloadableFound
|
||||
}
|
||||
switch {
|
||||
case msg.ImageMessage != nil:
|
||||
return cli.Download(msg.ImageMessage)
|
||||
case msg.VideoMessage != nil:
|
||||
return cli.Download(msg.VideoMessage)
|
||||
case msg.AudioMessage != nil:
|
||||
return cli.Download(msg.AudioMessage)
|
||||
case msg.DocumentMessage != nil:
|
||||
return cli.Download(msg.DocumentMessage)
|
||||
case msg.StickerMessage != nil:
|
||||
return cli.Download(msg.StickerMessage)
|
||||
default:
|
||||
return nil, ErrNothingDownloadableFound
|
||||
}
|
||||
return nil, ErrNothingDownloadableFound
|
||||
}
|
||||
|
||||
func getSize(msg DownloadableMessage) int {
|
||||
@@ -113,30 +147,63 @@ func getSize(msg DownloadableMessage) int {
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadThumbnail downloads a thumbnail from a message.
|
||||
//
|
||||
// This is primarily intended for downloading link preview thumbnails, which are in ExtendedTextMessage:
|
||||
// var msg *waProto.Message
|
||||
// ...
|
||||
// thumbnailImageBytes, err := cli.DownloadThumbnail(msg.GetExtendedTextMessage())
|
||||
func (cli *Client) DownloadThumbnail(msg DownloadableThumbnail) ([]byte, error) {
|
||||
mediaType, ok := classToThumbnailMediaType[msg.ProtoReflect().Descriptor().Name()]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w '%s'", ErrUnknownMediaType, string(msg.ProtoReflect().Descriptor().Name()))
|
||||
} else if len(msg.GetThumbnailDirectPath()) > 0 {
|
||||
return cli.DownloadMediaWithPath(msg.GetThumbnailDirectPath(), msg.GetThumbnailEncSha256(), msg.GetThumbnailSha256(), msg.GetMediaKey(), -1, mediaType, mediaTypeToMMSType[mediaType])
|
||||
} else {
|
||||
return nil, ErrNoURLPresent
|
||||
}
|
||||
}
|
||||
|
||||
// GetMediaType returns the MediaType value corresponding to the given protobuf message.
|
||||
func GetMediaType(msg DownloadableMessage) MediaType {
|
||||
return classToMediaType[msg.ProtoReflect().Descriptor().Name()]
|
||||
}
|
||||
|
||||
// Download downloads the attachment from the given protobuf message.
|
||||
func (cli *Client) Download(msg DownloadableMessage) (data []byte, err error) {
|
||||
//
|
||||
// The attachment is a specific part of a Message protobuf struct, not the message itself, e.g.
|
||||
// var msg *waProto.Message
|
||||
// ...
|
||||
// imageData, err := cli.Download(msg.GetImageMessage())
|
||||
//
|
||||
// You can also use DownloadAny to download the first non-nil sub-message.
|
||||
func (cli *Client) Download(msg DownloadableMessage) ([]byte, 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())
|
||||
return cli.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])
|
||||
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) {
|
||||
// DownloadMediaWithPath downloads an attachment by manually specifying the path and encryption details.
|
||||
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)
|
||||
}
|
||||
if len(mmsType) == 0 {
|
||||
mmsType = mediaTypeToMMSType[mediaType]
|
||||
}
|
||||
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)
|
||||
data, err = cli.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 {
|
||||
@@ -148,10 +215,10 @@ func (cli *Client) downloadMediaWithPath(directPath string, encFileHash, fileHas
|
||||
return
|
||||
}
|
||||
|
||||
func downloadAndDecrypt(url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSha256, fileSha256 []byte) (data []byte, err error) {
|
||||
func (cli *Client) downloadAndDecrypt(url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSha256, fileSha256 []byte) (data []byte, err error) {
|
||||
iv, cipherKey, macKey, _ := getMediaKeys(mediaKey, appInfo)
|
||||
var ciphertext, mac []byte
|
||||
if ciphertext, mac, err = downloadEncryptedMedia(url, fileEncSha256); err != nil {
|
||||
if ciphertext, mac, err = cli.downloadEncryptedMedia(url, fileEncSha256); err != nil {
|
||||
|
||||
} else if err = validateMedia(iv, ciphertext, macKey, mac); err != nil {
|
||||
|
||||
@@ -170,9 +237,17 @@ func getMediaKeys(mediaKey []byte, appInfo MediaType) (iv, cipherKey, macKey, re
|
||||
return mediaKeyExpanded[:16], mediaKeyExpanded[16:48], mediaKeyExpanded[48:80], mediaKeyExpanded[80:]
|
||||
}
|
||||
|
||||
func downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
|
||||
func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest(http.MethodGet, url, nil)
|
||||
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 resp *http.Response
|
||||
resp, err = http.Get(url)
|
||||
resp, err = cli.http.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
2
vendor/go.mau.fi/whatsmeow/errors.go
vendored
2
vendor/go.mau.fi/whatsmeow/errors.go
vendored
@@ -45,6 +45,8 @@ var (
|
||||
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")
|
||||
// ErrInvalidImageFormat is returned by SetGroupPhoto if the given photo is not in the correct format.
|
||||
ErrInvalidImageFormat = errors.New("the given data is not a valid image")
|
||||
)
|
||||
|
||||
// Some errors that Client.SendMessage can return
|
||||
|
||||
37
vendor/go.mau.fi/whatsmeow/group.go
vendored
37
vendor/go.mau.fi/whatsmeow/group.go
vendored
@@ -106,6 +106,40 @@ func (cli *Client) UpdateGroupParticipants(jid types.JID, participantChanges map
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SetGroupPhoto updates the group picture/icon of the given group on WhatsApp.
|
||||
// The avatar should be a JPEG photo, other formats may be rejected with ErrInvalidImageFormat.
|
||||
// The bytes can be nil to remove the photo. Returns the new picture ID.
|
||||
func (cli *Client) SetGroupPhoto(jid types.JID, avatar []byte) (string, error) {
|
||||
var content interface{}
|
||||
if avatar != nil {
|
||||
content = []waBinary.Node{{
|
||||
Tag: "picture",
|
||||
Attrs: waBinary.Attrs{"type": "image"},
|
||||
Content: avatar,
|
||||
}}
|
||||
}
|
||||
resp, err := cli.sendIQ(infoQuery{
|
||||
Namespace: "w:profile:picture",
|
||||
Type: iqSet,
|
||||
To: types.ServerJID,
|
||||
Target: jid,
|
||||
Content: content,
|
||||
})
|
||||
if errors.Is(err, ErrIQNotAcceptable) {
|
||||
return "", wrapIQError(ErrInvalidImageFormat, err)
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if avatar == nil {
|
||||
return "remove", nil
|
||||
}
|
||||
pictureID, ok := resp.GetChildByTag("picture").Attrs["id"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("didn't find picture ID in response")
|
||||
}
|
||||
return pictureID, 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{
|
||||
@@ -385,7 +419,8 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
|
||||
case "description":
|
||||
body, bodyOK := child.GetOptionalChildByTag("body")
|
||||
if bodyOK {
|
||||
group.Topic, _ = body.Content.(string)
|
||||
topicBytes, _ := body.Content.([]byte)
|
||||
group.Topic = string(topicBytes)
|
||||
group.TopicID = childAG.String("id")
|
||||
group.TopicSetBy = childAG.OptionalJIDOrEmpty("participant")
|
||||
group.TopicSetAt = time.Unix(childAG.Int64("t"), 0)
|
||||
|
||||
163
vendor/go.mau.fi/whatsmeow/mediaretry.go
vendored
Normal file
163
vendor/go.mau.fi/whatsmeow/mediaretry.go
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2022 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package whatsmeow
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"go.mau.fi/whatsmeow/util/hkdfutil"
|
||||
)
|
||||
|
||||
func getMediaRetryKey(mediaKey []byte) (cipherKey []byte) {
|
||||
return hkdfutil.SHA256(mediaKey, nil, []byte("WhatsApp Media Retry Notification"), 32)
|
||||
}
|
||||
|
||||
func prepareMediaRetryGCM(mediaKey []byte) (cipher.AEAD, error) {
|
||||
block, err := aes.NewCipher(getMediaRetryKey(mediaKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize AES cipher: %w", err)
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize GCM: %w", err)
|
||||
}
|
||||
return gcm, nil
|
||||
}
|
||||
|
||||
func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphertext, iv []byte, err error) {
|
||||
receipt := &waProto.ServerErrorReceipt{
|
||||
StanzaId: proto.String(messageID),
|
||||
}
|
||||
var plaintext []byte
|
||||
plaintext, err = proto.Marshal(receipt)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to marshal payload: %w", err)
|
||||
return
|
||||
}
|
||||
var gcm cipher.AEAD
|
||||
gcm, err = prepareMediaRetryGCM(mediaKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
iv = make([]byte, 12)
|
||||
_, err = rand.Read(iv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ciphertext = gcm.Seal(plaintext[:0], iv, plaintext, []byte(messageID))
|
||||
return
|
||||
}
|
||||
|
||||
// SendMediaRetryReceipt sends a request to the phone to re-upload the media in a message.
|
||||
//
|
||||
// The response will come as an *events.MediaRetry. The response will then have to be decrypted
|
||||
// using DecryptMediaRetryNotification and the same media key passed here.
|
||||
func (cli *Client) SendMediaRetryReceipt(message *types.MessageInfo, mediaKey []byte) error {
|
||||
ciphertext, iv, err := encryptMediaRetryReceipt(message.ID, mediaKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare encrypted retry receipt: %w", err)
|
||||
}
|
||||
|
||||
rmrAttrs := waBinary.Attrs{
|
||||
"jid": message.Chat,
|
||||
"from_me": message.IsFromMe,
|
||||
}
|
||||
if message.IsGroup {
|
||||
rmrAttrs["participant"] = message.Sender
|
||||
}
|
||||
|
||||
encryptedRequest := []waBinary.Node{
|
||||
{Tag: "enc_p", Content: ciphertext},
|
||||
{Tag: "enc_iv", Content: iv},
|
||||
}
|
||||
|
||||
err = cli.sendNode(waBinary.Node{
|
||||
Tag: "receipt",
|
||||
Attrs: waBinary.Attrs{
|
||||
"id": message.ID,
|
||||
"to": cli.Store.ID.ToNonAD(),
|
||||
"type": "server-error",
|
||||
},
|
||||
Content: []waBinary.Node{
|
||||
{Tag: "encrypt", Content: encryptedRequest},
|
||||
{Tag: "rmr", Attrs: rmrAttrs},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecryptMediaRetryNotification decrypts a media retry notification using the media key.
|
||||
func DecryptMediaRetryNotification(evt *events.MediaRetry, mediaKey []byte) (*waProto.MediaRetryNotification, error) {
|
||||
gcm, err := prepareMediaRetryGCM(mediaKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintext, err := gcm.Open(nil, evt.IV, evt.Ciphertext, []byte(evt.MessageID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt notification: %w", err)
|
||||
}
|
||||
var notif waProto.MediaRetryNotification
|
||||
err = proto.Unmarshal(plaintext, ¬if)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal notification (invalid encryption key?): %w", err)
|
||||
}
|
||||
return ¬if, nil
|
||||
}
|
||||
|
||||
func parseMediaRetryNotification(node *waBinary.Node) (*events.MediaRetry, error) {
|
||||
ag := node.AttrGetter()
|
||||
var evt events.MediaRetry
|
||||
evt.Timestamp = time.Unix(ag.Int64("t"), 0)
|
||||
evt.MessageID = types.MessageID(ag.String("id"))
|
||||
if !ag.OK() {
|
||||
return nil, ag.Error()
|
||||
}
|
||||
rmr, ok := node.GetOptionalChildByTag("rmr")
|
||||
if !ok {
|
||||
return nil, &ElementMissingError{Tag: "rmr", In: "retry notification"}
|
||||
}
|
||||
rmrAG := rmr.AttrGetter()
|
||||
evt.ChatID = rmrAG.JID("jid")
|
||||
evt.FromMe = rmrAG.Bool("from_me")
|
||||
evt.SenderID = rmrAG.OptionalJIDOrEmpty("participant")
|
||||
if !rmrAG.OK() {
|
||||
return nil, fmt.Errorf("missing attributes in <rmr> tag: %w", rmrAG.Error())
|
||||
}
|
||||
|
||||
evt.Ciphertext, ok = node.GetChildByTag("encrypt", "enc_p").Content.([]byte)
|
||||
if !ok {
|
||||
return nil, &ElementMissingError{Tag: "enc_p", In: fmt.Sprintf("retry notification %s", evt.MessageID)}
|
||||
}
|
||||
evt.IV, ok = node.GetChildByTag("encrypt", "enc_iv").Content.([]byte)
|
||||
if !ok {
|
||||
return nil, &ElementMissingError{Tag: "enc_iv", In: fmt.Sprintf("retry notification %s", evt.MessageID)}
|
||||
}
|
||||
return &evt, nil
|
||||
}
|
||||
|
||||
func (cli *Client) handleMediaRetryNotification(node *waBinary.Node) {
|
||||
// TODO handle errors (e.g. <error code="2"/>)
|
||||
evt, err := parseMediaRetryNotification(node)
|
||||
if err != nil {
|
||||
cli.Log.Warnf("Failed to parse media retry notification: %v", err)
|
||||
return
|
||||
}
|
||||
cli.dispatchEvent(evt)
|
||||
}
|
||||
3
vendor/go.mau.fi/whatsmeow/message.go
vendored
3
vendor/go.mau.fi/whatsmeow/message.go
vendored
@@ -283,6 +283,7 @@ func (cli *Client) handleHistorySyncNotification(notif *waProto.HistorySyncNotif
|
||||
}
|
||||
|
||||
func (cli *Client) handleAppStateSyncKeyShare(keys *waProto.AppStateSyncKeyShare) {
|
||||
cli.Log.Debugf("Got %d new app state keys", len(keys.GetKeys()))
|
||||
for _, key := range keys.GetKeys() {
|
||||
marshaledFingerprint, err := proto.Marshal(key.GetKeyData().GetFingerprint())
|
||||
if err != nil {
|
||||
@@ -365,7 +366,7 @@ func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.
|
||||
}
|
||||
|
||||
func (cli *Client) sendProtocolMessageReceipt(id, msgType string) {
|
||||
if len(id) == 0 {
|
||||
if len(id) == 0 || cli.Store.ID == nil {
|
||||
return
|
||||
}
|
||||
err := cli.sendNode(waBinary.Node{
|
||||
|
||||
3
vendor/go.mau.fi/whatsmeow/notification.go
vendored
3
vendor/go.mau.fi/whatsmeow/notification.go
vendored
@@ -199,6 +199,9 @@ func (cli *Client) handleNotification(node *waBinary.Node) {
|
||||
}
|
||||
case "picture":
|
||||
go cli.handlePictureNotification(node)
|
||||
case "mediaretry":
|
||||
go cli.handleMediaRetryNotification(node)
|
||||
// Other types: business, disappearing_mode, server, status, pay, psa, privacy_token
|
||||
default:
|
||||
cli.Log.Debugf("Unhandled notification with type %s", notifType)
|
||||
}
|
||||
|
||||
22
vendor/go.mau.fi/whatsmeow/presence.go
vendored
22
vendor/go.mau.fi/whatsmeow/presence.go
vendored
@@ -7,6 +7,7 @@
|
||||
package whatsmeow
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
waBinary "go.mau.fi/whatsmeow/binary"
|
||||
@@ -23,12 +24,14 @@ func (cli *Client) handleChatState(node *waBinary.Node) {
|
||||
} else {
|
||||
child := node.GetChildren()[0]
|
||||
presence := types.ChatPresence(child.Tag)
|
||||
if presence != types.ChatPresenceComposing && presence != types.ChatPresenceRecording && presence != types.ChatPresencePaused {
|
||||
if presence != types.ChatPresenceComposing && presence != types.ChatPresencePaused {
|
||||
cli.Log.Warnf("Unrecognized chat presence state %s", child.Tag)
|
||||
}
|
||||
media := types.ChatPresenceMedia(child.AttrGetter().OptionalString("media"))
|
||||
cli.dispatchEvent(&events.ChatPresence{
|
||||
MessageSource: source,
|
||||
State: presence,
|
||||
Media: media,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -62,6 +65,11 @@ func (cli *Client) SendPresence(state types.Presence) error {
|
||||
if len(cli.Store.PushName) == 0 {
|
||||
return ErrNoPushName
|
||||
}
|
||||
if state == types.PresenceAvailable {
|
||||
atomic.CompareAndSwapUint32(&cli.sendActiveReceipts, 0, 1)
|
||||
} else {
|
||||
atomic.CompareAndSwapUint32(&cli.sendActiveReceipts, 1, 0)
|
||||
}
|
||||
return cli.sendNode(waBinary.Node{
|
||||
Tag: "presence",
|
||||
Attrs: waBinary.Attrs{
|
||||
@@ -89,13 +97,21 @@ func (cli *Client) SubscribePresence(jid types.JID) error {
|
||||
}
|
||||
|
||||
// SendChatPresence updates the user's typing status in a specific chat.
|
||||
func (cli *Client) SendChatPresence(state types.ChatPresence, jid types.JID) error {
|
||||
//
|
||||
// The media parameter can be set to indicate the user is recording media (like a voice message) rather than typing a text message.
|
||||
func (cli *Client) SendChatPresence(jid types.JID, state types.ChatPresence, media types.ChatPresenceMedia) error {
|
||||
content := []waBinary.Node{{Tag: string(state)}}
|
||||
if state == types.ChatPresenceComposing && len(media) > 0 {
|
||||
content[0].Attrs = waBinary.Attrs{
|
||||
"media": string(media),
|
||||
}
|
||||
}
|
||||
return cli.sendNode(waBinary.Node{
|
||||
Tag: "chatstate",
|
||||
Attrs: waBinary.Attrs{
|
||||
"from": *cli.Store.ID,
|
||||
"to": jid,
|
||||
},
|
||||
Content: []waBinary.Node{{Tag: string(state)}},
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
8
vendor/go.mau.fi/whatsmeow/qrchan.go
vendored
8
vendor/go.mau.fi/whatsmeow/qrchan.go
vendored
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021 Tulir Asokan
|
||||
// Copyright (c) 2022 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
|
||||
@@ -36,6 +36,8 @@ var (
|
||||
// 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"}
|
||||
// QRChannelClientOutdated is emitted from GetQRChannel if events.ClientOutdated is received.
|
||||
QRChannelClientOutdated = QRChannelItem{Event: "err-client-outdated"}
|
||||
// QRChannelScannedWithoutMultidevice is emitted from GetQRChannel if events.QRScannedWithoutMultidevice is received.
|
||||
QRChannelScannedWithoutMultidevice = QRChannelItem{Event: "err-scanned-without-multidevice"}
|
||||
)
|
||||
@@ -117,6 +119,8 @@ func (qrc *qrChannel) handleEvent(rawEvt interface{}) {
|
||||
qrc.log.Debugf("QR code scanned without multidevice enabled")
|
||||
qrc.output <- QRChannelScannedWithoutMultidevice
|
||||
return
|
||||
case *events.ClientOutdated:
|
||||
outputType = QRChannelClientOutdated
|
||||
case *events.PairSuccess:
|
||||
outputType = QRChannelSuccess
|
||||
case *events.PairError:
|
||||
@@ -126,7 +130,7 @@ func (qrc *qrChannel) handleEvent(rawEvt interface{}) {
|
||||
}
|
||||
case *events.Disconnected:
|
||||
outputType = QRChannelTimeout
|
||||
case *events.Connected, *events.ConnectFailure, *events.LoggedOut:
|
||||
case *events.Connected, *events.ConnectFailure, *events.LoggedOut, *events.TemporaryBan:
|
||||
outputType = QRChannelErrUnexpectedEvent
|
||||
default:
|
||||
return
|
||||
|
||||
28
vendor/go.mau.fi/whatsmeow/receipt.go
vendored
28
vendor/go.mau.fi/whatsmeow/receipt.go
vendored
@@ -8,6 +8,7 @@ package whatsmeow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
waBinary "go.mau.fi/whatsmeow/binary"
|
||||
@@ -123,13 +124,35 @@ func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, se
|
||||
return cli.sendNode(node)
|
||||
}
|
||||
|
||||
// SetForceActiveDeliveryReceipts will force the client to send normal delivery
|
||||
// receipts (which will show up as the two gray ticks on WhatsApp), even if the
|
||||
// client isn't marked as online.
|
||||
//
|
||||
// By default, clients that haven't been marked as online will send delivery
|
||||
// receipts with type="inactive", which is transmitted to the sender, but not
|
||||
// rendered in the official WhatsApp apps. This is consistent with how WhatsApp
|
||||
// web works when it's not in the foreground.
|
||||
//
|
||||
// To mark the client as online, use
|
||||
// cli.SendPresence(types.PresenceAvailable)
|
||||
//
|
||||
// Note that if you turn this off (i.e. call SetForceActiveDeliveryReceipts(false)),
|
||||
// receipts will act like the client is offline until SendPresence is called again.
|
||||
func (cli *Client) SetForceActiveDeliveryReceipts(active bool) {
|
||||
if active {
|
||||
atomic.StoreUint32(&cli.sendActiveReceipts, 2)
|
||||
} else {
|
||||
atomic.StoreUint32(&cli.sendActiveReceipts, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *Client) sendMessageReceipt(info *types.MessageInfo) {
|
||||
attrs := waBinary.Attrs{
|
||||
"id": info.ID,
|
||||
}
|
||||
if info.IsFromMe {
|
||||
attrs["type"] = "sender"
|
||||
} else {
|
||||
} else if atomic.LoadUint32(&cli.sendActiveReceipts) == 0 {
|
||||
attrs["type"] = "inactive"
|
||||
}
|
||||
attrs["to"] = info.Chat
|
||||
@@ -137,6 +160,9 @@ func (cli *Client) sendMessageReceipt(info *types.MessageInfo) {
|
||||
attrs["participant"] = info.Sender
|
||||
} else if info.IsFromMe {
|
||||
attrs["recipient"] = info.Sender
|
||||
} else {
|
||||
// Override the to attribute with the JID version with a device number
|
||||
attrs["to"] = info.Sender
|
||||
}
|
||||
err := cli.sendNode(waBinary.Node{
|
||||
Tag: "receipt",
|
||||
|
||||
6
vendor/go.mau.fi/whatsmeow/request.go
vendored
6
vendor/go.mau.fi/whatsmeow/request.go
vendored
@@ -78,6 +78,7 @@ type infoQuery struct {
|
||||
Namespace string
|
||||
Type infoQueryType
|
||||
To types.JID
|
||||
Target types.JID
|
||||
ID string
|
||||
Content interface{}
|
||||
|
||||
@@ -98,6 +99,9 @@ func (cli *Client) sendIQAsync(query infoQuery) (<-chan *waBinary.Node, error) {
|
||||
if !query.To.IsEmpty() {
|
||||
attrs["to"] = query.To
|
||||
}
|
||||
if !query.Target.IsEmpty() {
|
||||
attrs["target"] = query.Target
|
||||
}
|
||||
err := cli.sendNode(waBinary.Node{
|
||||
Tag: "iq",
|
||||
Attrs: attrs,
|
||||
@@ -116,7 +120,7 @@ func (cli *Client) sendIQ(query infoQuery) (*waBinary.Node, error) {
|
||||
return nil, err
|
||||
}
|
||||
if query.Timeout == 0 {
|
||||
query.Timeout = 1 * time.Minute
|
||||
query.Timeout = 75 * time.Second
|
||||
}
|
||||
if query.Context == nil {
|
||||
query.Context = context.Background()
|
||||
|
||||
19
vendor/go.mau.fi/whatsmeow/send.go
vendored
19
vendor/go.mau.fi/whatsmeow/send.go
vendored
@@ -30,6 +30,9 @@ import (
|
||||
)
|
||||
|
||||
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
|
||||
//
|
||||
// msgID := whatsmeow.GenerateMessageID()
|
||||
// cli.SendMessage(targetJID, msgID, &waProto.Message{...})
|
||||
func GenerateMessageID() types.MessageID {
|
||||
id := make([]byte, 16)
|
||||
_, err := rand.Read(id)
|
||||
@@ -46,6 +49,20 @@ func GenerateMessageID() types.MessageID {
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// The message itself can contain anything you want (within the protobuf schema).
|
||||
// e.g. for a simple text message, use the Conversation field:
|
||||
// cli.SendMessage(targetJID, "", &waProto.Message{
|
||||
// Conversation: proto.String("Hello, World!"),
|
||||
// })
|
||||
//
|
||||
// Things like replies, mentioning users and the "forwarded" flag are stored in ContextInfo,
|
||||
// which can be put in ExtendedTextMessage and any of the media message types.
|
||||
//
|
||||
// For uploading and sending media/attachments, see the Upload method.
|
||||
//
|
||||
// For other message types, you'll have to figure it out yourself. Looking at the protobuf schema
|
||||
// in binary/proto/def.proto may be useful to find out all the allowed fields.
|
||||
func (cli *Client) SendMessage(to types.JID, id types.MessageID, message *waProto.Message) (time.Time, error) {
|
||||
if to.AD {
|
||||
return time.Time{}, ErrRecipientADJID
|
||||
@@ -210,7 +227,7 @@ func (cli *Client) prepareMessageNode(to types.JID, id types.MessageID, message
|
||||
Content: participantNodes,
|
||||
}},
|
||||
}
|
||||
if message.ProtocolMessage != nil && message.GetProtocolMessage().GetType() == waProto.ProtocolMessage_REVOKE {
|
||||
if message.ProtocolMessage != nil && message.GetProtocolMessage().GetType() == waProto.ProtocolMessage_REVOKE && message.GetProtocolMessage().GetKey() != nil {
|
||||
node.Attrs["edit"] = "7"
|
||||
}
|
||||
if includeIdentity {
|
||||
|
||||
11
vendor/go.mau.fi/whatsmeow/socket/constants.go
vendored
11
vendor/go.mau.fi/whatsmeow/socket/constants.go
vendored
@@ -10,7 +10,11 @@
|
||||
// The Client struct in the top-level whatsmeow package handles everything.
|
||||
package socket
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"go.mau.fi/whatsmeow/binary/token"
|
||||
)
|
||||
|
||||
const (
|
||||
// Origin is the Origin header for all WhatsApp websocket connections
|
||||
@@ -22,11 +26,10 @@ const (
|
||||
const (
|
||||
NoiseStartPattern = "Noise_XX_25519_AESGCM_SHA256\x00\x00\x00\x00"
|
||||
|
||||
WADictVersion = 2
|
||||
WAMagicValue = 5
|
||||
WAMagicValue = 5
|
||||
)
|
||||
|
||||
var WAConnHeader = []byte{'W', 'A', WAMagicValue, WADictVersion}
|
||||
var WAConnHeader = []byte{'W', 'A', WAMagicValue, token.DictVersion}
|
||||
|
||||
const (
|
||||
FrameMaxSize = 2 << 23
|
||||
|
||||
12
vendor/go.mau.fi/whatsmeow/socket/framesocket.go
vendored
12
vendor/go.mau.fi/whatsmeow/socket/framesocket.go
vendored
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -19,6 +20,8 @@ import (
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
)
|
||||
|
||||
type Proxy = func(*http.Request) (*url.URL, error)
|
||||
|
||||
type FrameSocket struct {
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
@@ -31,6 +34,7 @@ type FrameSocket struct {
|
||||
WriteTimeout time.Duration
|
||||
|
||||
Header []byte
|
||||
Proxy Proxy
|
||||
|
||||
incomingLength int
|
||||
receivedLength int
|
||||
@@ -38,12 +42,14 @@ type FrameSocket struct {
|
||||
partialHeader []byte
|
||||
}
|
||||
|
||||
func NewFrameSocket(log waLog.Logger, header []byte) *FrameSocket {
|
||||
func NewFrameSocket(log waLog.Logger, header []byte, proxy Proxy) *FrameSocket {
|
||||
return &FrameSocket{
|
||||
conn: nil,
|
||||
log: log,
|
||||
Header: header,
|
||||
Frames: make(chan []byte),
|
||||
|
||||
Proxy: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +98,9 @@ func (fs *FrameSocket) Connect() error {
|
||||
return ErrSocketAlreadyOpen
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
dialer := websocket.Dialer{}
|
||||
dialer := websocket.Dialer{
|
||||
Proxy: fs.Proxy,
|
||||
}
|
||||
|
||||
headers := http.Header{"Origin": []string{Origin}}
|
||||
fs.log.Debugf("Dialing %s", URL)
|
||||
|
||||
@@ -20,36 +20,98 @@ import (
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
)
|
||||
|
||||
// WAVersionContainer is a container for a WhatsApp web version number.
|
||||
type WAVersionContainer [3]uint32
|
||||
|
||||
// ParseVersion parses a version string (three dot-separated numbers) into a WAVersionContainer.
|
||||
func ParseVersion(version string) (parsed WAVersionContainer, err error) {
|
||||
var part1, part2, part3 int
|
||||
if parts := strings.Split(version, "."); len(parts) != 3 {
|
||||
err = fmt.Errorf("'%s' doesn't contain three dot-separated parts", version)
|
||||
} else if part1, err = strconv.Atoi(parts[0]); err != nil {
|
||||
err = fmt.Errorf("first part of '%s' is not a number: %w", version, err)
|
||||
} else if part2, err = strconv.Atoi(parts[1]); err != nil {
|
||||
err = fmt.Errorf("second part of '%s' is not a number: %w", version, err)
|
||||
} else if part3, err = strconv.Atoi(parts[2]); err != nil {
|
||||
err = fmt.Errorf("third part of '%s' is not a number: %w", version, err)
|
||||
} else {
|
||||
parsed = WAVersionContainer{uint32(part1), uint32(part2), uint32(part3)}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (vc WAVersionContainer) LessThan(other WAVersionContainer) bool {
|
||||
return vc[0] < other[0] ||
|
||||
(vc[0] == other[0] && vc[1] < other[1]) ||
|
||||
(vc[0] == other[0] && vc[1] == other[1] && vc[2] < other[2])
|
||||
}
|
||||
|
||||
// IsZero returns true if the version is zero.
|
||||
func (vc WAVersionContainer) IsZero() bool {
|
||||
return vc == [3]uint32{0, 0, 0}
|
||||
}
|
||||
|
||||
// String returns the version number as a dot-separated string.
|
||||
func (vc WAVersionContainer) String() string {
|
||||
parts := make([]string, len(vc))
|
||||
for i, part := range vc {
|
||||
parts[i] = strconv.Itoa(int(part))
|
||||
}
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
// Hash returns the md5 hash of the String representation of this version.
|
||||
func (vc WAVersionContainer) Hash() [16]byte {
|
||||
return md5.Sum([]byte(vc.String()))
|
||||
}
|
||||
|
||||
func (vc WAVersionContainer) ProtoAppVersion() *waProto.AppVersion {
|
||||
return &waProto.AppVersion{
|
||||
Primary: &vc[0],
|
||||
Secondary: &vc[1],
|
||||
Tertiary: &vc[2],
|
||||
}
|
||||
}
|
||||
|
||||
// waVersion is the WhatsApp web client version
|
||||
var waVersion = []uint32{2, 2202, 9}
|
||||
var waVersion = WAVersionContainer{2, 2208, 7}
|
||||
|
||||
// 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))
|
||||
waVersionHash = waVersion.Hash()
|
||||
}
|
||||
|
||||
// GetWAVersion gets the current WhatsApp web client version.
|
||||
func GetWAVersion() WAVersionContainer {
|
||||
return waVersion
|
||||
}
|
||||
|
||||
// SetWAVersion sets the current WhatsApp web client version.
|
||||
//
|
||||
// In general, you should keep the library up-to-date instead of using this,
|
||||
// as there may be code changes that are necessary too (like protobuf schema changes).
|
||||
func SetWAVersion(version WAVersionContainer) {
|
||||
if version.IsZero() {
|
||||
return
|
||||
}
|
||||
waVersionString := strings.Join(waVersionParts, ".")
|
||||
waVersionHash = md5.Sum([]byte(waVersionString))
|
||||
waVersion = version
|
||||
waVersionHash = version.Hash()
|
||||
}
|
||||
|
||||
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"),
|
||||
AppVersion: waVersion.ProtoAppVersion(),
|
||||
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"),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021 Tulir Asokan
|
||||
// Copyright (c) 2022 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
|
||||
@@ -349,15 +349,15 @@ func (s *SQLStore) putAppStateMutationMACs(tx execable, name string, version uin
|
||||
values[0] = s.JID
|
||||
values[1] = name
|
||||
values[2] = version
|
||||
placeholderSyntax := "($1, $2, $3, $%d, $%d)"
|
||||
if s.dialect == "sqlite3" {
|
||||
placeholderSyntax = "(?1, ?2, ?3, ?%d, ?%d)"
|
||||
}
|
||||
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)
|
||||
}
|
||||
queryParts[i] = fmt.Sprintf(placeholderSyntax, baseIndex+1, baseIndex+2)
|
||||
}
|
||||
_, err := tx.Exec(putAppStateMutationMACsQuery+strings.Join(queryParts, ","), values...)
|
||||
return err
|
||||
@@ -426,7 +426,12 @@ func (s *SQLStore) GetAppStateMutationMAC(name string, indexMAC []byte) (valueMA
|
||||
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
|
||||
ON CONFLICT (our_jid, their_jid) DO UPDATE SET first_name=excluded.first_name, full_name=excluded.full_name
|
||||
`
|
||||
putManyContactNamesQuery = `
|
||||
INSERT INTO whatsmeow_contacts (our_jid, their_jid, first_name, full_name)
|
||||
VALUES %s
|
||||
ON CONFLICT (our_jid, their_jid) DO UPDATE SET first_name=excluded.first_name, full_name=excluded.full_name
|
||||
`
|
||||
putPushNameQuery = `
|
||||
INSERT INTO whatsmeow_contacts (our_jid, their_jid, push_name) VALUES ($1, $2, $3)
|
||||
@@ -504,6 +509,77 @@ func (s *SQLStore) PutContactName(user types.JID, firstName, fullName string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
const contactBatchSize = 300
|
||||
|
||||
func (s *SQLStore) putContactNamesBatch(tx execable, contacts []store.ContactEntry) error {
|
||||
values := make([]interface{}, 1, 1+len(contacts)*3)
|
||||
queryParts := make([]string, 0, len(contacts))
|
||||
values[0] = s.JID
|
||||
placeholderSyntax := "($1, $%d, $%d, $%d)"
|
||||
if s.dialect == "sqlite3" {
|
||||
placeholderSyntax = "(?1, ?%d, ?%d, ?%d)"
|
||||
}
|
||||
i := 0
|
||||
handledContacts := make(map[types.JID]struct{}, len(contacts))
|
||||
for _, contact := range contacts {
|
||||
if contact.JID.IsEmpty() {
|
||||
s.log.Warnf("Empty contact info in mass insert: %+v", contact)
|
||||
continue
|
||||
}
|
||||
// The whole query will break if there are duplicates, so make sure there aren't any duplicates
|
||||
_, alreadyHandled := handledContacts[contact.JID]
|
||||
if alreadyHandled {
|
||||
s.log.Warnf("Duplicate contact info for %s in mass insert", contact.JID)
|
||||
continue
|
||||
}
|
||||
handledContacts[contact.JID] = struct{}{}
|
||||
baseIndex := i*3 + 1
|
||||
values = append(values, contact.JID.String(), contact.FirstName, contact.FullName)
|
||||
queryParts = append(queryParts, fmt.Sprintf(placeholderSyntax, baseIndex+1, baseIndex+2, baseIndex+3))
|
||||
i++
|
||||
}
|
||||
_, err := tx.Exec(fmt.Sprintf(putManyContactNamesQuery, strings.Join(queryParts, ",")), values...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLStore) PutAllContactNames(contacts []store.ContactEntry) error {
|
||||
if len(contacts) > contactBatchSize {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start transaction: %w", err)
|
||||
}
|
||||
for i := 0; i < len(contacts); i += contactBatchSize {
|
||||
var contactSlice []store.ContactEntry
|
||||
if len(contacts) > i+contactBatchSize {
|
||||
contactSlice = contacts[i : i+contactBatchSize]
|
||||
} else {
|
||||
contactSlice = contacts[i:]
|
||||
}
|
||||
err = s.putContactNamesBatch(tx, contactSlice)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
} else if len(contacts) > 0 {
|
||||
err := s.putContactNamesBatch(s.db, contacts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
s.contactCacheLock.Lock()
|
||||
// Just clear the cache, fetching pushnames and business names would be too much effort
|
||||
s.contactCache = make(map[types.JID]*types.ContactInfo)
|
||||
s.contactCacheLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getContact(user types.JID) (*types.ContactInfo, error) {
|
||||
cached, ok := s.contactCache[user]
|
||||
if ok {
|
||||
|
||||
9
vendor/go.mau.fi/whatsmeow/store/store.go
vendored
9
vendor/go.mau.fi/whatsmeow/store/store.go
vendored
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021 Tulir Asokan
|
||||
// Copyright (c) 2022 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
|
||||
@@ -71,10 +71,17 @@ type AppStateStore interface {
|
||||
GetAppStateMutationMAC(name string, indexMAC []byte) (valueMAC []byte, err error)
|
||||
}
|
||||
|
||||
type ContactEntry struct {
|
||||
JID types.JID
|
||||
FirstName string
|
||||
FullName string
|
||||
}
|
||||
|
||||
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
|
||||
PutAllContactNames(contacts []ContactEntry) error
|
||||
GetContact(user types.JID) (types.ContactInfo, error)
|
||||
GetAllContacts() (map[types.JID]types.ContactInfo, error)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ type Archive struct {
|
||||
Action *waProto.ArchiveChatAction // The current archival status of the chat.
|
||||
}
|
||||
|
||||
// MarkChatAsRead is emitted when a whole chat is marked as read or unread from another device.
|
||||
type MarkChatAsRead struct {
|
||||
JID types.JID // The chat which was marked as read or unread.
|
||||
Timestamp time.Time // The time when the marking happened.
|
||||
|
||||
Action *waProto.MarkChatAsReadAction // Whether the chat was marked as read or unread, and info about the most recent messages.
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
104
vendor/go.mau.fi/whatsmeow/types/events/events.go
vendored
104
vendor/go.mau.fi/whatsmeow/types/events/events.go
vendored
@@ -62,6 +62,8 @@ 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
|
||||
// If OnConnect is true, then this field contains the reason code.
|
||||
Reason ConnectFailureReason
|
||||
}
|
||||
|
||||
// StreamReplaced is emitted when the client is disconnected by another client connecting with the same keys.
|
||||
@@ -70,14 +72,96 @@ type LoggedOut struct {
|
||||
// or otherwise try to connect twice with the same session.
|
||||
type StreamReplaced struct{}
|
||||
|
||||
// TempBanReason is an error code included in temp ban error events.
|
||||
type TempBanReason int
|
||||
|
||||
const (
|
||||
TempBanBlockedByUsers TempBanReason = 101
|
||||
TempBanSentToTooManyPeople TempBanReason = 102
|
||||
TempBanCreatedTooManyGroups TempBanReason = 103
|
||||
TempBanSentTooManySameMessage TempBanReason = 104
|
||||
TempBanBroadcastList TempBanReason = 106
|
||||
)
|
||||
|
||||
var tempBanReasonMessage = map[TempBanReason]string{
|
||||
TempBanBlockedByUsers: "too many people blocked you",
|
||||
TempBanSentToTooManyPeople: "you sent too many messages to people who don't have you in their address books",
|
||||
TempBanCreatedTooManyGroups: "you created too many groups with people who don't have you in their address books",
|
||||
TempBanSentTooManySameMessage: "you sent the same message to too many people",
|
||||
TempBanBroadcastList: "you sent too many messages to a broadcast list",
|
||||
}
|
||||
|
||||
// String returns the reason code and a human-readable description of the ban reason.
|
||||
func (tbr TempBanReason) String() string {
|
||||
msg, ok := tempBanReasonMessage[tbr]
|
||||
if !ok {
|
||||
msg = "you may have violated the terms of service (unknown error)"
|
||||
}
|
||||
return fmt.Sprintf("%d: %s", int(tbr), msg)
|
||||
}
|
||||
|
||||
// TemporaryBan is emitted when there's a connection failure with the ConnectFailureTempBanned reason code.
|
||||
type TemporaryBan struct {
|
||||
Code TempBanReason
|
||||
Expire time.Time
|
||||
}
|
||||
|
||||
func (tb *TemporaryBan) String() string {
|
||||
if tb.Expire.IsZero() {
|
||||
return fmt.Sprintf("You've been temporarily banned: %v", tb.Code)
|
||||
}
|
||||
return fmt.Sprintf("You've been temporarily banned: %v. The ban expires at %v", tb.Code, tb.Expire)
|
||||
}
|
||||
|
||||
// ConnectFailureReason is an error code included in connection failure events.
|
||||
type ConnectFailureReason int
|
||||
|
||||
const (
|
||||
ConnectFailureLoggedOut ConnectFailureReason = 401
|
||||
ConnectFailureTempBanned ConnectFailureReason = 402
|
||||
ConnectFailureBanned ConnectFailureReason = 403
|
||||
ConnectFailureUnknownLogout ConnectFailureReason = 406
|
||||
|
||||
ConnectFailureClientOutdated ConnectFailureReason = 405
|
||||
ConnectFailureBadUserAgent ConnectFailureReason = 409
|
||||
|
||||
// 400, 500 and 501 are also existing codes, but the meaning is unknown
|
||||
)
|
||||
|
||||
var connectFailureReasonMessage = map[ConnectFailureReason]string{
|
||||
ConnectFailureLoggedOut: "logged out from another device",
|
||||
ConnectFailureTempBanned: "account temporarily banned",
|
||||
ConnectFailureBanned: "account banned from WhatsApp",
|
||||
ConnectFailureUnknownLogout: "logged out for unknown reason",
|
||||
ConnectFailureClientOutdated: "client is out of date",
|
||||
ConnectFailureBadUserAgent: "client user agent was rejected",
|
||||
}
|
||||
|
||||
// IsLoggedOut returns true if the client should delete session data due to this connect failure.
|
||||
func (cfr ConnectFailureReason) IsLoggedOut() bool {
|
||||
return cfr == ConnectFailureLoggedOut || cfr == ConnectFailureBanned || cfr == ConnectFailureUnknownLogout
|
||||
}
|
||||
|
||||
// String returns the reason code and a short human-readable description of the error.
|
||||
func (cfr ConnectFailureReason) String() string {
|
||||
msg, ok := connectFailureReasonMessage[cfr]
|
||||
if !ok {
|
||||
msg = "unknown error"
|
||||
}
|
||||
return fmt.Sprintf("%d: %s", int(cfr), msg)
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Known reasons are handled internally and emitted as different events (e.g. LoggedOut and TemporaryBan).
|
||||
type ConnectFailure struct {
|
||||
Reason string
|
||||
Reason ConnectFailureReason
|
||||
Raw *waBinary.Node
|
||||
}
|
||||
|
||||
// ClientOutdated is emitted when the WhatsApp server rejects the connection with the ConnectFailureClientOutdated code.
|
||||
type ClientOutdated struct{}
|
||||
|
||||
// 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).
|
||||
@@ -165,7 +249,8 @@ type Receipt struct {
|
||||
// client.SendPresence(types.PresenceAvailable)
|
||||
type ChatPresence struct {
|
||||
types.MessageSource
|
||||
State types.ChatPresence
|
||||
State types.ChatPresence // The current state, either composing or paused
|
||||
Media types.ChatPresenceMedia // When composing, the type of message
|
||||
}
|
||||
|
||||
// Presence is emitted when a presence update is received.
|
||||
@@ -261,3 +346,16 @@ type OfflineSyncPreview struct {
|
||||
type OfflineSyncCompleted struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// MediaRetry is emitted when the phone sends a response to a media retry request.
|
||||
type MediaRetry struct {
|
||||
Ciphertext []byte
|
||||
IV []byte
|
||||
|
||||
Timestamp time.Time // The time of the response.
|
||||
|
||||
MessageID types.MessageID // The ID of the message.
|
||||
ChatID types.JID // The chat ID where the message was sent.
|
||||
SenderID types.JID // The user who sent the message. Only present in groups.
|
||||
FromMe bool // Whether the message was sent by the current user or someone else.
|
||||
}
|
||||
|
||||
15
vendor/go.mau.fi/whatsmeow/types/jid.go
vendored
15
vendor/go.mau.fi/whatsmeow/types/jid.go
vendored
@@ -155,6 +155,21 @@ func (jid JID) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler for JID
|
||||
func (jid JID) MarshalText() ([]byte, error) {
|
||||
return []byte(jid.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler for JID
|
||||
func (jid *JID) UnmarshalText(val []byte) error {
|
||||
out, err := ParseJID(string(val))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*jid = out
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
8
vendor/go.mau.fi/whatsmeow/types/presence.go
vendored
8
vendor/go.mau.fi/whatsmeow/types/presence.go
vendored
@@ -17,6 +17,12 @@ type ChatPresence string
|
||||
|
||||
const (
|
||||
ChatPresenceComposing ChatPresence = "composing"
|
||||
ChatPresenceRecording ChatPresence = "recording"
|
||||
ChatPresencePaused ChatPresence = "paused"
|
||||
)
|
||||
|
||||
type ChatPresenceMedia string
|
||||
|
||||
const (
|
||||
ChatPresenceMediaText ChatPresenceMedia = ""
|
||||
ChatPresenceMediaAudio ChatPresenceMedia = "audio"
|
||||
)
|
||||
|
||||
81
vendor/go.mau.fi/whatsmeow/update.go
vendored
Normal file
81
vendor/go.mau.fi/whatsmeow/update.go
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2022 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/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"go.mau.fi/whatsmeow/socket"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
)
|
||||
|
||||
// CheckUpdateResponse is the data returned by CheckUpdate.
|
||||
type CheckUpdateResponse struct {
|
||||
IsBroken bool
|
||||
IsBelowSoft bool
|
||||
IsBelowHard bool
|
||||
CurrentVersion string
|
||||
|
||||
ParsedVersion store.WAVersionContainer `json:"-"`
|
||||
}
|
||||
|
||||
// CheckUpdateURL is the base URL to check for WhatsApp web updates.
|
||||
const CheckUpdateURL = "https://web.whatsapp.com/check-update"
|
||||
|
||||
// CheckUpdate asks the WhatsApp servers if there is an update available
|
||||
// (using the HTTP client and proxy settings of this whatsmeow Client instance).
|
||||
func (cli *Client) CheckUpdate() (respData CheckUpdateResponse, err error) {
|
||||
return CheckUpdate(http.DefaultClient)
|
||||
}
|
||||
|
||||
// CheckUpdate asks the WhatsApp servers if there is an update available.
|
||||
func CheckUpdate(httpClient *http.Client) (respData CheckUpdateResponse, err error) {
|
||||
var reqURL *url.URL
|
||||
reqURL, err = url.Parse(CheckUpdateURL)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse check update URL: %w", err)
|
||||
return
|
||||
}
|
||||
q := reqURL.Query()
|
||||
q.Set("version", store.GetWAVersion().String())
|
||||
q.Set("platform", "web")
|
||||
reqURL.RawQuery = q.Encode()
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest(http.MethodGet, reqURL.String(), nil)
|
||||
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 resp *http.Response
|
||||
resp, err = httpClient.Do(req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to send request: %w", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = fmt.Errorf("unexpected response with status %d: %s", resp.StatusCode, body)
|
||||
return
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&respData)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to decode response body (status %d): %w", resp.StatusCode, err)
|
||||
return
|
||||
}
|
||||
respData.ParsedVersion, err = store.ParseVersion(respData.CurrentVersion)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse version string: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
39
vendor/go.mau.fi/whatsmeow/upload.go
vendored
39
vendor/go.mau.fi/whatsmeow/upload.go
vendored
@@ -24,16 +24,41 @@ import (
|
||||
|
||||
// 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
|
||||
URL string `json:"url"`
|
||||
DirectPath string `json:"direct_path"`
|
||||
|
||||
MediaKey []byte
|
||||
FileEncSHA256 []byte
|
||||
FileSHA256 []byte
|
||||
FileLength uint64
|
||||
MediaKey []byte `json:"-"`
|
||||
FileEncSHA256 []byte `json:"-"`
|
||||
FileSHA256 []byte `json:"-"`
|
||||
FileLength uint64 `json:"-"`
|
||||
}
|
||||
|
||||
// Upload uploads the given attachment to WhatsApp servers.
|
||||
//
|
||||
// You should copy the fields in the response to the corresponding fields in a protobuf message.
|
||||
//
|
||||
// For example, to send an image:
|
||||
// resp, err := cli.Upload(context.Background(), yourImageBytes, whatsmeow.MediaImage)
|
||||
// // handle error
|
||||
//
|
||||
// imageMsg := &waProto.ImageMessage{
|
||||
// Caption: proto.String("Hello, world!"),
|
||||
// Mimetype: proto.String("image/png"), // replace this with the actual mime type
|
||||
// // you can also optionally add other fields like ContextInfo and JpegThumbnail here
|
||||
//
|
||||
// Url: &resp.URL,
|
||||
// DirectPath: &uploaded.DirectPath,
|
||||
// MediaKey: resp.MediaKey,
|
||||
// FileEncSha256: resp.FileEncSHA256,
|
||||
// FileSha256: resp.FileSha256,
|
||||
// FileLength: &resp.FileLength,
|
||||
// }
|
||||
// _, err = cli.SendMessage(targetJID, "", &waProto.Message{
|
||||
// ImageMessage: imageMsg,
|
||||
// })
|
||||
// // handle error again
|
||||
//
|
||||
// The same applies to the other message types like DocumentMessage, just replace the struct type and Message field name.
|
||||
func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaType) (resp UploadResponse, err error) {
|
||||
resp.FileLength = uint64(len(plaintext))
|
||||
resp.MediaKey = make([]byte, 32)
|
||||
@@ -92,7 +117,7 @@ func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaTy
|
||||
req.Header.Set("Referer", socket.Origin+"/")
|
||||
|
||||
var httpResp *http.Response
|
||||
httpResp, err = http.DefaultClient.Do(req)
|
||||
httpResp, err = cli.http.Do(req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to execute request: %w", err)
|
||||
} else if httpResp.StatusCode != http.StatusOK {
|
||||
|
||||
Reference in New Issue
Block a user