Skip to content

Commit 5ab97aa

Browse files
Merge pull request #9 from evermos/feat-update-whatsmeow-version
patch: update whatsmeow version
2 parents bcd73c4 + 07b57fc commit 5ab97aa

File tree

87 files changed

+8042
-5155
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+8042
-5155
lines changed

README.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ discussions.
1414

1515
## Usage
1616
The [godoc](https://pkg.go.dev/go.mau.fi/whatsmeow) includes docs for all methods and event types.
17-
There's also a [simple example](https://godocs.io/go.mau.fi/whatsmeow#example-package) at the top.
18-
19-
Also see [mdtest](./mdtest) for a CLI tool you can easily try out whatsmeow with.
17+
There's also a [simple example](https://pkg.go.dev/go.mau.fi/whatsmeow#example-package) at the top.
2018

2119
## Features
2220
Most core features are already present:

binary/encoder.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (w *binaryEncoder) writeNode(n Node) {
9090
hasContent = 1
9191
}
9292

93-
w.writeListStart(2*len(n.Attrs) + tagSize + hasContent)
93+
w.writeListStart(2*w.countAttributes(n.Attrs) + tagSize + hasContent)
9494
w.writeString(n.Tag)
9595
w.writeAttributes(n.Attrs)
9696
if n.Content != nil {
@@ -187,10 +187,6 @@ func (w *binaryEncoder) writeJID(jid types.JID) {
187187
}
188188

189189
func (w *binaryEncoder) writeAttributes(attributes Attrs) {
190-
if attributes == nil {
191-
return
192-
}
193-
194190
for key, val := range attributes {
195191
if val == "" || val == nil {
196192
continue
@@ -201,6 +197,16 @@ func (w *binaryEncoder) writeAttributes(attributes Attrs) {
201197
}
202198
}
203199

200+
func (w *binaryEncoder) countAttributes(attributes Attrs) (count int) {
201+
for _, val := range attributes {
202+
if val == "" || val == nil {
203+
continue
204+
}
205+
count += 1
206+
}
207+
return
208+
}
209+
204210
func (w *binaryEncoder) writeListStart(listSize int) {
205211
if listSize == 0 {
206212
w.pushByte(byte(token.ListEmpty))

binary/proto/legacy.go

-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ type (
175175
ContextInfo = waE2E.ContextInfo
176176
ForwardedNewsletterMessageInfo = waE2E.ContextInfo_ForwardedNewsletterMessageInfo
177177
BotSuggestedPromptMetadata = waE2E.BotSuggestedPromptMetadata
178-
BotSearchMetadata = waE2E.BotSearchMetadata
179178
BotPluginMetadata = waE2E.BotPluginMetadata
180179
BotMetadata = waE2E.BotMetadata
181180
BotAvatarMetadata = waE2E.BotAvatarMetadata

client.go

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ type Client struct {
7070
// the client will not attempt to reconnect. The number of retries can be read from AutoReconnectErrors.
7171
AutoReconnectHook func(error) bool
7272

73+
DisableLoginAutoReconnect bool
74+
7375
sendActiveReceipts atomic.Uint32
7476

7577
// EmitAppStateEventsOnFullSync can be set to true if you want to get app state events emitted
@@ -247,6 +249,7 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
247249
}
248250
cli.nodeHandlers = map[string]nodeHandler{
249251
"message": cli.handleEncryptedMessage,
252+
"appdata": cli.handleEncryptedMessage,
250253
"receipt": cli.handleReceipt,
251254
"call": cli.handleCallEvent,
252255
"chatstate": cli.handleChatState,

client_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ func eventHandler(evt interface{}) {
2727
}
2828

2929
func Example() {
30+
// |------------------------------------------------------------------------------------------------------|
31+
// | NOTE: You must also import the appropriate DB connector, e.g. github.com/mattn/go-sqlite3 for SQLite |
32+
// |------------------------------------------------------------------------------------------------------|
33+
3034
dbLog := waLog.Stdout("Database", "DEBUG", true)
31-
// Make sure you add appropriate DB connector imports, e.g. github.com/mattn/go-sqlite3 for SQLite
3235
container, err := sqlstore.New("sqlite3", "file:examplestore.db?_foreign_keys=on", dbLog)
3336
if err != nil {
3437
panic(err)

connectionevents.go

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ func (cli *Client) handleStreamError(node *waBinary.Node) {
2323
conflictType := conflict.AttrGetter().OptionalString("type")
2424
switch {
2525
case code == "515":
26+
if cli.DisableLoginAutoReconnect {
27+
cli.Log.Infof("Got 515 code, but login autoreconnect is disabled, not reconnecting")
28+
cli.dispatchEvent(&events.ManualLoginReconnect{})
29+
return
30+
}
2631
cli.Log.Infof("Got 515 code, reconnecting...")
2732
go func() {
2833
cli.Disconnect()

download-to-file.go

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright (c) 2024 Tulir Asokan
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
7+
package whatsmeow
8+
9+
import (
10+
"crypto/hmac"
11+
"crypto/sha256"
12+
"encoding/base64"
13+
"errors"
14+
"fmt"
15+
"io"
16+
"os"
17+
"strings"
18+
"time"
19+
20+
"go.mau.fi/util/fallocate"
21+
"go.mau.fi/util/retryafter"
22+
23+
"go.mau.fi/whatsmeow/proto/waMediaTransport"
24+
"go.mau.fi/whatsmeow/util/cbcutil"
25+
)
26+
27+
type File interface {
28+
io.Reader
29+
io.Writer
30+
io.Seeker
31+
io.ReaderAt
32+
io.WriterAt
33+
Truncate(size int64) error
34+
Stat() (os.FileInfo, error)
35+
}
36+
37+
// DownloadToFile downloads the attachment from the given protobuf message.
38+
//
39+
// This is otherwise identical to [Download], but writes the attachment to a file instead of returning it as a byte slice.
40+
func (cli *Client) DownloadToFile(msg DownloadableMessage, file File) error {
41+
mediaType, ok := classToMediaType[msg.ProtoReflect().Descriptor().Name()]
42+
if !ok {
43+
return fmt.Errorf("%w '%s'", ErrUnknownMediaType, string(msg.ProtoReflect().Descriptor().Name()))
44+
}
45+
urlable, ok := msg.(downloadableMessageWithURL)
46+
var url string
47+
var isWebWhatsappNetURL bool
48+
if ok {
49+
url = urlable.GetUrl()
50+
isWebWhatsappNetURL = strings.HasPrefix(url, "https://web.whatsapp.net")
51+
}
52+
if len(url) > 0 && !isWebWhatsappNetURL {
53+
return cli.downloadAndDecryptToFile(url, msg.GetMediaKey(), mediaType, getSize(msg), msg.GetFileEncSHA256(), msg.GetFileSHA256(), file)
54+
} else if len(msg.GetDirectPath()) > 0 {
55+
return cli.DownloadMediaWithPathToFile(msg.GetDirectPath(), msg.GetFileEncSHA256(), msg.GetFileSHA256(), msg.GetMediaKey(), getSize(msg), mediaType, mediaTypeToMMSType[mediaType], file)
56+
} else {
57+
if isWebWhatsappNetURL {
58+
cli.Log.Warnf("Got a media message with a web.whatsapp.net URL (%s) and no direct path", url)
59+
}
60+
return ErrNoURLPresent
61+
}
62+
}
63+
64+
func (cli *Client) DownloadFBToFile(transport *waMediaTransport.WAMediaTransport_Integral, mediaType MediaType, file File) error {
65+
return cli.DownloadMediaWithPathToFile(transport.GetDirectPath(), transport.GetFileEncSHA256(), transport.GetFileSHA256(), transport.GetMediaKey(), -1, mediaType, mediaTypeToMMSType[mediaType], file)
66+
}
67+
68+
func (cli *Client) DownloadMediaWithPathToFile(directPath string, encFileHash, fileHash, mediaKey []byte, fileLength int, mediaType MediaType, mmsType string, file File) error {
69+
mediaConn, err := cli.refreshMediaConn(false)
70+
if err != nil {
71+
return fmt.Errorf("failed to refresh media connections: %w", err)
72+
}
73+
if len(mmsType) == 0 {
74+
mmsType = mediaTypeToMMSType[mediaType]
75+
}
76+
for i, host := range mediaConn.Hosts {
77+
// TODO omit hash for unencrypted media?
78+
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
79+
err = cli.downloadAndDecryptToFile(mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash, file)
80+
if err == nil || errors.Is(err, ErrFileLengthMismatch) || errors.Is(err, ErrInvalidMediaSHA256) {
81+
return err
82+
} else if i >= len(mediaConn.Hosts)-1 {
83+
return fmt.Errorf("failed to download media from last host: %w", err)
84+
}
85+
// TODO there are probably some errors that shouldn't retry
86+
cli.Log.Warnf("Failed to download media: %s, trying with next host...", err)
87+
}
88+
return err
89+
}
90+
91+
func (cli *Client) downloadAndDecryptToFile(url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSHA256, fileSHA256 []byte, file File) error {
92+
iv, cipherKey, macKey, _ := getMediaKeys(mediaKey, appInfo)
93+
hasher := sha256.New()
94+
if mac, err := cli.downloadPossiblyEncryptedMediaWithRetriesToFile(url, fileEncSHA256, file); err != nil {
95+
return err
96+
} else if mediaKey == nil && fileEncSHA256 == nil && mac == nil {
97+
// Unencrypted media, just return the downloaded data
98+
return nil
99+
} else if err = validateMediaFile(file, iv, macKey, mac); err != nil {
100+
return err
101+
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
102+
return fmt.Errorf("failed to seek to start of file after validating mac: %w", err)
103+
} else if err = cbcutil.DecryptFile(cipherKey, iv, file); err != nil {
104+
return fmt.Errorf("failed to decrypt file: %w", err)
105+
} else if info, err := file.Stat(); err != nil {
106+
return fmt.Errorf("failed to stat file: %w", err)
107+
} else if info.Size() != int64(fileLength) {
108+
return fmt.Errorf("%w: expected %d, got %d", ErrFileLengthMismatch, fileLength, info.Size())
109+
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
110+
return fmt.Errorf("failed to seek to start of file after decrypting: %w", err)
111+
} else if _, err = io.Copy(hasher, file); err != nil {
112+
return fmt.Errorf("failed to hash file: %w", err)
113+
} else if !hmac.Equal(fileSHA256, hasher.Sum(nil)) {
114+
return ErrInvalidMediaSHA256
115+
}
116+
return nil
117+
}
118+
119+
func (cli *Client) downloadPossiblyEncryptedMediaWithRetriesToFile(url string, checksum []byte, file File) (mac []byte, err error) {
120+
for retryNum := 0; retryNum < 5; retryNum++ {
121+
if checksum == nil {
122+
_, _, err = cli.downloadMediaToFile(url, file)
123+
} else {
124+
mac, err = cli.downloadEncryptedMediaToFile(url, checksum, file)
125+
}
126+
if err == nil || !shouldRetryMediaDownload(err) {
127+
return
128+
}
129+
retryDuration := time.Duration(retryNum+1) * time.Second
130+
var httpErr DownloadHTTPError
131+
if errors.As(err, &httpErr) {
132+
retryDuration = retryafter.Parse(httpErr.Response.Header.Get("Retry-After"), retryDuration)
133+
}
134+
cli.Log.Warnf("Failed to download media due to network error: %v, retrying in %s...", err, retryDuration)
135+
time.Sleep(retryDuration)
136+
}
137+
return
138+
}
139+
140+
func (cli *Client) downloadMediaToFile(url string, file io.Writer) (int64, []byte, error) {
141+
resp, err := cli.doMediaDownloadRequest(url)
142+
if err != nil {
143+
return 0, nil, err
144+
}
145+
defer resp.Body.Close()
146+
osFile, ok := file.(*os.File)
147+
if ok && resp.ContentLength > 0 {
148+
err = fallocate.Fallocate(osFile, int(resp.ContentLength))
149+
if err != nil {
150+
return 0, nil, fmt.Errorf("failed to preallocate file: %w", err)
151+
}
152+
}
153+
hasher := sha256.New()
154+
n, err := io.Copy(file, io.TeeReader(resp.Body, hasher))
155+
return n, hasher.Sum(nil), err
156+
}
157+
158+
func (cli *Client) downloadEncryptedMediaToFile(url string, checksum []byte, file File) ([]byte, error) {
159+
size, hash, err := cli.downloadMediaToFile(url, file)
160+
if err != nil {
161+
return nil, err
162+
} else if size <= mediaHMACLength {
163+
return nil, ErrTooShortFile
164+
} else if len(checksum) == 32 && !hmac.Equal(checksum, hash) {
165+
return nil, ErrInvalidMediaEncSHA256
166+
}
167+
mac := make([]byte, mediaHMACLength)
168+
_, err = file.ReadAt(mac, size-mediaHMACLength)
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to read MAC from file: %w", err)
171+
}
172+
err = file.Truncate(size - mediaHMACLength)
173+
if err != nil {
174+
return nil, fmt.Errorf("failed to truncate file to remove MAC: %w", err)
175+
}
176+
return mac, nil
177+
}
178+
179+
func validateMediaFile(file io.ReadSeeker, iv, macKey, mac []byte) error {
180+
h := hmac.New(sha256.New, macKey)
181+
h.Write(iv)
182+
_, err := file.Seek(0, io.SeekStart)
183+
if err != nil {
184+
return fmt.Errorf("failed to seek to start of file: %w", err)
185+
}
186+
_, err = io.Copy(h, file)
187+
if err != nil {
188+
return fmt.Errorf("failed to hash file: %w", err)
189+
}
190+
if !hmac.Equal(h.Sum(nil)[:mediaHMACLength], mac) {
191+
return ErrInvalidMediaHMAC
192+
}
193+
return nil
194+
}

download.go

+20-8
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHas
229229
// TODO omit hash for unencrypted media?
230230
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
231231
data, err = cli.downloadAndDecrypt(mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash)
232-
if err == nil {
232+
if err == nil || errors.Is(err, ErrFileLengthMismatch) || errors.Is(err, ErrInvalidMediaSHA256) {
233233
return
234234
} else if i >= len(mediaConn.Hosts)-1 {
235235
return nil, fmt.Errorf("failed to download media from last host: %w", err)
@@ -288,13 +288,13 @@ func (cli *Client) downloadPossiblyEncryptedMediaWithRetries(url string, checksu
288288
if errors.As(err, &httpErr) {
289289
retryDuration = retryafter.Parse(httpErr.Response.Header.Get("Retry-After"), retryDuration)
290290
}
291-
cli.Log.Warnf("Failed to download media due to network error: %w, retrying in %s...", err, retryDuration)
291+
cli.Log.Warnf("Failed to download media due to network error: %v, retrying in %s...", err, retryDuration)
292292
time.Sleep(retryDuration)
293293
}
294294
return
295295
}
296296

297-
func (cli *Client) downloadMedia(url string) ([]byte, error) {
297+
func (cli *Client) doMediaDownloadRequest(url string) (*http.Response, error) {
298298
req, err := http.NewRequest(http.MethodGet, url, nil)
299299
if err != nil {
300300
return nil, fmt.Errorf("failed to prepare request: %w", err)
@@ -309,22 +309,34 @@ func (cli *Client) downloadMedia(url string) ([]byte, error) {
309309
if err != nil {
310310
return nil, err
311311
}
312-
defer resp.Body.Close()
313312
if resp.StatusCode != http.StatusOK {
313+
_ = resp.Body.Close()
314314
return nil, DownloadHTTPError{Response: resp}
315315
}
316-
return io.ReadAll(resp.Body)
316+
return resp, nil
317+
}
318+
319+
func (cli *Client) downloadMedia(url string) ([]byte, error) {
320+
resp, err := cli.doMediaDownloadRequest(url)
321+
if err != nil {
322+
return nil, err
323+
}
324+
data, err := io.ReadAll(resp.Body)
325+
_ = resp.Body.Close()
326+
return data, err
317327
}
318328

329+
const mediaHMACLength = 10
330+
319331
func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
320332
data, err := cli.downloadMedia(url)
321333
if err != nil {
322334
return
323-
} else if len(data) <= 10 {
335+
} else if len(data) <= mediaHMACLength {
324336
err = ErrTooShortFile
325337
return
326338
}
327-
file, mac = data[:len(data)-10], data[len(data)-10:]
339+
file, mac = data[:len(data)-mediaHMACLength], data[len(data)-mediaHMACLength:]
328340
if len(checksum) == 32 && sha256.Sum256(data) != *(*[32]byte)(checksum) {
329341
err = ErrInvalidMediaEncSHA256
330342
}
@@ -335,7 +347,7 @@ func validateMedia(iv, file, macKey, mac []byte) error {
335347
h := hmac.New(sha256.New, macKey)
336348
h.Write(iv)
337349
h.Write(file)
338-
if !hmac.Equal(h.Sum(nil)[:10], mac) {
350+
if !hmac.Equal(h.Sum(nil)[:mediaHMACLength], mac) {
339351
return ErrInvalidMediaHMAC
340352
}
341353
return nil

errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ var (
106106
ErrUnknownServer = errors.New("can't send message to unknown server")
107107
ErrRecipientADJID = errors.New("message recipient must be a user JID with no device part")
108108
ErrServerReturnedError = errors.New("server returned error")
109+
ErrInvalidInlineBotID = errors.New("invalid inline bot ID")
109110
)
110111

111112
type DownloadHTTPError struct {

go.mod

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ go 1.21
55
require (
66
github.com/google/uuid v1.6.0
77
github.com/gorilla/websocket v1.5.0
8-
github.com/rs/zerolog v1.32.0
9-
go.mau.fi/libsignal v0.1.0
10-
go.mau.fi/util v0.4.1
11-
golang.org/x/crypto v0.23.0
12-
golang.org/x/net v0.25.0
13-
google.golang.org/protobuf v1.33.0
8+
github.com/rs/zerolog v1.33.0
9+
go.mau.fi/libsignal v0.1.1
10+
go.mau.fi/util v0.6.0
11+
golang.org/x/crypto v0.25.0
12+
golang.org/x/net v0.27.0
13+
google.golang.org/protobuf v1.34.2
1414
)
1515

1616
require (
17-
filippo.io/edwards25519 v1.0.0 // indirect
17+
filippo.io/edwards25519 v1.1.0 // indirect
1818
github.com/mattn/go-colorable v0.1.13 // indirect
1919
github.com/mattn/go-isatty v0.0.19 // indirect
20-
golang.org/x/sys v0.20.0 // indirect
20+
golang.org/x/sys v0.22.0 // indirect
2121
)

0 commit comments

Comments
 (0)