Skip to content

Commit

Permalink
Add initial newsletter support
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Oct 13, 2023
1 parent 2788e7c commit a99d1b5
Show file tree
Hide file tree
Showing 14 changed files with 752 additions and 31 deletions.
41 changes: 23 additions & 18 deletions download.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHas
mmsType = mediaTypeToMMSType[mediaType]
}
for i, host := range mediaConn.Hosts {
// TODO omit hash for unencrypted media?
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
data, err = cli.downloadAndDecrypt(mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash)
// TODO there are probably some errors that shouldn't retry
Expand All @@ -237,8 +238,11 @@ func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHas
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 = cli.downloadEncryptedMediaWithRetries(url, fileEncSha256); err != nil {
if ciphertext, mac, err = cli.downloadPossiblyEncryptedMediaWithRetries(url, fileEncSha256); err != nil {

} else if mediaKey == nil && fileEncSha256 == nil && mac == nil {
// Unencrypted media, just return the downloaded data
data = ciphertext
} else if err = validateMedia(iv, ciphertext, macKey, mac); err != nil {

} else if data, err = cbcutil.Decrypt(cipherKey, iv, ciphertext); err != nil {
Expand All @@ -263,9 +267,13 @@ func shouldRetryMediaDownload(err error) bool {
(errors.As(err, &httpErr) && retryafter.Should(httpErr.StatusCode, true))
}

func (cli *Client) downloadEncryptedMediaWithRetries(url string, checksum []byte) (file, mac []byte, err error) {
func (cli *Client) downloadPossiblyEncryptedMediaWithRetries(url string, checksum []byte) (file, mac []byte, err error) {
for retryNum := 0; retryNum < 5; retryNum++ {
file, mac, err = cli.downloadEncryptedMedia(url, checksum)
if checksum == nil {
file, err = cli.downloadMedia(url)
} else {
file, mac, err = cli.downloadEncryptedMedia(url, checksum)
}
if err == nil || !shouldRetryMediaDownload(err) {
return
}
Expand All @@ -280,30 +288,27 @@ func (cli *Client) downloadEncryptedMediaWithRetries(url string, checksum []byte
return
}

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)
func (cli *Client) downloadMedia(url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
err = fmt.Errorf("failed to prepare request: %w", err)
return
return nil, fmt.Errorf("failed to prepare request: %w", err)
}
req.Header.Set("Origin", socket.Origin)
req.Header.Set("Referer", socket.Origin+"/")
var resp *http.Response
resp, err = cli.http.Do(req)
resp, err := cli.http.Do(req)
if err != nil {
return
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = DownloadHTTPError{Response: resp}
return
return nil, DownloadHTTPError{Response: resp}
}
var data []byte
data, err = io.ReadAll(resp.Body)
if err != nil {
return
} else if len(data) <= 10 {
return io.ReadAll(resp.Body)
}

func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
data, err := cli.downloadMedia(url)
if len(data) <= 10 {
err = ErrTooShortFile
return
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.20
require (
github.com/gorilla/websocket v1.5.0
go.mau.fi/libsignal v0.1.0
go.mau.fi/util v0.1.0
go.mau.fi/util v0.1.1-0.20231013112707-e938021823cc
golang.org/x/crypto v0.13.0
google.golang.org/protobuf v1.31.0
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
go.mau.fi/util v0.1.1-0.20231013112707-e938021823cc h1:/ZY5g+McWqVSA6fK8ROBOyJFb5hCBBQKMcm2oRFzc9c=
go.mau.fi/util v0.1.1-0.20231013112707-e938021823cc/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
Expand Down
2 changes: 1 addition & 1 deletion mdtest/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
go.mau.fi/libsignal v0.1.0 // indirect
go.mau.fi/util v0.1.0 // indirect
go.mau.fi/util v0.1.1-0.20231013112707-e938021823cc // indirect
golang.org/x/crypto v0.13.0 // indirect
rsc.io/qr v0.2.0 // indirect
)
Expand Down
4 changes: 2 additions & 2 deletions mdtest/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
go.mau.fi/util v0.1.1-0.20231013112707-e938021823cc h1:/ZY5g+McWqVSA6fK8ROBOyJFb5hCBBQKMcm2oRFzc9c=
go.mau.fi/util v0.1.1-0.20231013112707-e938021823cc/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
71 changes: 71 additions & 0 deletions mdtest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,77 @@ func handleCmd(cmd string, args []string) {
} else {
log.Infof("Node sent")
}
case "getnewsletter":
jid, ok := parseJID(args[0])
if !ok {
return
}
meta, err := cli.GetNewsletterInfo(jid)
if err != nil {
log.Errorf("Failed to get info: %v", err)
} else {
log.Infof("Got info: %+v", meta)
}
case "livesubscribenewsletter":
if len(args) < 1 {
log.Errorf("Usage: livesubscribenewsletter <jid>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
dur, err := cli.SubscribeNewsletterLiveUpdates(context.TODO(), jid)
if err != nil {
log.Errorf("Failed to subscribe to live updates: %v", err)
} else {
log.Infof("Subscribed to live updates for %s for %s", jid, dur)
}
case "getnewslettermessages":
if len(args) < 1 {
log.Errorf("Usage: getnewslettermessages <jid> [count] [before id]")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
count := 100
var err error
if len(args) > 1 {
count, err = strconv.Atoi(args[1])
if err != nil {
log.Errorf("Invalid count: %v", err)
return
}
}
var before types.MessageServerID
if len(args) > 2 {
before, err = strconv.Atoi(args[2])
if err != nil {
log.Errorf("Invalid message ID: %v", err)
return
}
}
messages, err := cli.GetNewsletterMessages(jid, &whatsmeow.GetNewsletterMessagesParams{Count: count, Before: before})
if err != nil {
log.Errorf("Failed to get messages: %v", err)
} else {
for _, msg := range messages {
log.Infof("%d: %+v (viewed %d times)", msg.MessageServerID, msg.Message, msg.ViewsCount)
}
}
case "createnewsletter":
if len(args) < 1 {
log.Errorf("Usage: createnewsletter <name>")
return
}
err := cli.CreateNewsletter(strings.Join(args, " "), "")
if err != nil {
log.Errorf("Failed to create newsletter: %v", err)
} else {
log.Infof("Created newsletter?")
}
case "getavatar":
if len(args) < 1 {
log.Errorf("Usage: getavatar <jid> [existing ID] [--preview] [--community]")
Expand Down
38 changes: 36 additions & 2 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ func (cli *Client) handleEncryptedMessage(node *waBinary.Node) {
if len(info.PushName) > 0 && info.PushName != "-" {
go cli.updatePushName(info.Sender, info, info.PushName)
}
cli.decryptMessages(info, node)
go cli.sendAck(node)
if info.Sender.Server == types.NewsletterServer {
cli.handlePlaintextMessage(info, node)
} else {
cli.decryptMessages(info, node)
}
}
}

Expand All @@ -71,6 +76,10 @@ func (cli *Client) parseMessageSource(node *waBinary.Node, requireParticipant bo
if from.Server == types.BroadcastServer {
source.BroadcastListOwner = ag.OptionalJIDOrEmpty("recipient")
}
} else if from.Server == types.NewsletterServer {
source.Chat = from
source.Sender = from
// TODO IsFromMe?
} else if from.User == clientID.User {
source.IsFromMe = true
source.Sender = from
Expand All @@ -97,6 +106,7 @@ func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, er
}
ag := node.AttrGetter()
info.ID = types.MessageID(ag.String("id"))
info.ServerID = types.MessageServerID(ag.OptionalInt("server_id"))
info.Timestamp = ag.UnixTime("t")
info.PushName = ag.OptionalString("notify")
info.Category = ag.OptionalString("category")
Expand All @@ -121,14 +131,38 @@ func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, er
return &info, nil
}

func (cli *Client) handlePlaintextMessage(info *types.MessageInfo, node *waBinary.Node) {
// TODO edits have an additional <meta msg_edit_t="1696321271735" original_msg_t="1696321248"/> node
plaintext, ok := node.GetOptionalChildByTag("plaintext")
if !ok {
// 3:
return
}
plaintextBody, ok := plaintext.Content.([]byte)
if !ok {
cli.Log.Warnf("Plaintext message from %s doesn't have byte content", info.SourceString())
return
}
var msg waProto.Message
err := proto.Unmarshal(plaintextBody, &msg)
if err != nil {
cli.Log.Warnf("Error unmarshaling plaintext message from %s: %v", info.SourceString(), err)
return
}
cli.handleDecryptedMessage(info, &msg, 0)
// TODO do these need receipts?
//go cli.sendMessageReceipt(info)
return
}

func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node) {
go cli.sendAck(node)
if len(node.GetChildrenByTag("unavailable")) > 0 && len(node.GetChildrenByTag("enc")) == 0 {
cli.Log.Warnf("Unavailable message %s from %s", info.ID, info.SourceString())
go cli.sendRetryReceipt(node, info, 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
Expand Down
Loading

0 comments on commit a99d1b5

Please sign in to comment.