mediaretry.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // Copyright (c) 2022 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. package whatsmeow
  7. import (
  8. "context"
  9. "fmt"
  10. "go.mau.fi/util/random"
  11. "google.golang.org/protobuf/proto"
  12. waBinary "git.bobomao.top/joey/testwh/binary"
  13. "git.bobomao.top/joey/testwh/proto/waMmsRetry"
  14. "git.bobomao.top/joey/testwh/types"
  15. "git.bobomao.top/joey/testwh/types/events"
  16. "git.bobomao.top/joey/testwh/util/gcmutil"
  17. "git.bobomao.top/joey/testwh/util/hkdfutil"
  18. )
  19. func getMediaRetryKey(mediaKey []byte) (cipherKey []byte) {
  20. return hkdfutil.SHA256(mediaKey, nil, []byte("WhatsApp Media Retry Notification"), 32)
  21. }
  22. func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphertext, iv []byte, err error) {
  23. receipt := &waMmsRetry.ServerErrorReceipt{
  24. StanzaID: proto.String(messageID),
  25. }
  26. var plaintext []byte
  27. plaintext, err = proto.Marshal(receipt)
  28. if err != nil {
  29. err = fmt.Errorf("failed to marshal payload: %w", err)
  30. return
  31. }
  32. iv = random.Bytes(12)
  33. ciphertext, err = gcmutil.Encrypt(getMediaRetryKey(mediaKey), iv, plaintext, []byte(messageID))
  34. return
  35. }
  36. // SendMediaRetryReceipt sends a request to the phone to re-upload the media in a message.
  37. //
  38. // This is mostly relevant when handling history syncs and getting a 404 or 410 error downloading media.
  39. // Rough example on how to use it (will not work out of the box, you must adjust it depending on what you need exactly):
  40. //
  41. // var mediaRetryCache map[types.MessageID]*waE2E.ImageMessage
  42. //
  43. // evt, err := cli.ParseWebMessage(chatJID, historyMsg.GetMessage())
  44. // imageMsg := evt.Message.GetImageMessage() // replace this with the part of the message you want to download
  45. // data, err := cli.Download(imageMsg)
  46. // if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
  47. // err = cli.SendMediaRetryReceipt(&evt.Info, imageMsg.GetMediaKey())
  48. // // You need to store the event data somewhere as it's necessary for handling the retry response.
  49. // mediaRetryCache[evt.Info.ID] = imageMsg
  50. // }
  51. //
  52. // The response will come as an *events.MediaRetry. The response will then have to be decrypted
  53. // using DecryptMediaRetryNotification and the same media key passed here. If the media retry was successful,
  54. // the decrypted notification should contain an updated DirectPath, which can be used to download the file.
  55. //
  56. // func eventHandler(rawEvt interface{}) {
  57. // switch evt := rawEvt.(type) {
  58. // case *events.MediaRetry:
  59. // imageMsg := mediaRetryCache[evt.MessageID]
  60. // retryData, err := whatsmeow.DecryptMediaRetryNotification(evt, imageMsg.GetMediaKey())
  61. // if err != nil || retryData.GetResult != waMmsRetry.MediaRetryNotification_SUCCESS {
  62. // return
  63. // }
  64. // // Use the new path to download the attachment
  65. // imageMsg.DirectPath = retryData.DirectPath
  66. // data, err := cli.Download(imageMsg)
  67. // // Alternatively, you can use cli.DownloadMediaWithPath and provide the individual fields manually.
  68. // }
  69. // }
  70. func (cli *Client) SendMediaRetryReceipt(ctx context.Context, message *types.MessageInfo, mediaKey []byte) error {
  71. if cli == nil {
  72. return ErrClientIsNil
  73. }
  74. ciphertext, iv, err := encryptMediaRetryReceipt(message.ID, mediaKey)
  75. if err != nil {
  76. return fmt.Errorf("failed to prepare encrypted retry receipt: %w", err)
  77. }
  78. ownID := cli.getOwnID().ToNonAD()
  79. if ownID.IsEmpty() {
  80. return ErrNotLoggedIn
  81. }
  82. rmrAttrs := waBinary.Attrs{
  83. "jid": message.Chat,
  84. "from_me": message.IsFromMe,
  85. }
  86. if message.IsGroup {
  87. rmrAttrs["participant"] = message.Sender
  88. }
  89. encryptedRequest := []waBinary.Node{
  90. {Tag: "enc_p", Content: ciphertext},
  91. {Tag: "enc_iv", Content: iv},
  92. }
  93. err = cli.sendNode(ctx, waBinary.Node{
  94. Tag: "receipt",
  95. Attrs: waBinary.Attrs{
  96. "id": message.ID,
  97. "to": ownID,
  98. "type": "server-error",
  99. },
  100. Content: []waBinary.Node{
  101. {Tag: "encrypt", Content: encryptedRequest},
  102. {Tag: "rmr", Attrs: rmrAttrs},
  103. },
  104. })
  105. if err != nil {
  106. return err
  107. }
  108. return nil
  109. }
  110. // DecryptMediaRetryNotification decrypts a media retry notification using the media key.
  111. // See Client.SendMediaRetryReceipt for more info on how to use this.
  112. func DecryptMediaRetryNotification(evt *events.MediaRetry, mediaKey []byte) (*waMmsRetry.MediaRetryNotification, error) {
  113. var notif waMmsRetry.MediaRetryNotification
  114. if evt.Error != nil && evt.Ciphertext == nil {
  115. if evt.Error.Code == 2 {
  116. return nil, ErrMediaNotAvailableOnPhone
  117. }
  118. return nil, fmt.Errorf("%w (code: %d)", ErrUnknownMediaRetryError, evt.Error.Code)
  119. } else if plaintext, err := gcmutil.Decrypt(getMediaRetryKey(mediaKey), evt.IV, evt.Ciphertext, []byte(evt.MessageID)); err != nil {
  120. return nil, fmt.Errorf("failed to decrypt notification: %w", err)
  121. } else if err = proto.Unmarshal(plaintext, &notif); err != nil {
  122. return nil, fmt.Errorf("failed to unmarshal notification (invalid encryption key?): %w", err)
  123. } else {
  124. return &notif, nil
  125. }
  126. }
  127. func parseMediaRetryNotification(node *waBinary.Node) (*events.MediaRetry, error) {
  128. ag := node.AttrGetter()
  129. var evt events.MediaRetry
  130. evt.Timestamp = ag.UnixTime("t")
  131. evt.MessageID = types.MessageID(ag.String("id"))
  132. if !ag.OK() {
  133. return nil, ag.Error()
  134. }
  135. rmr, ok := node.GetOptionalChildByTag("rmr")
  136. if !ok {
  137. return nil, &ElementMissingError{Tag: "rmr", In: "retry notification"}
  138. }
  139. rmrAG := rmr.AttrGetter()
  140. evt.ChatID = rmrAG.JID("jid")
  141. evt.FromMe = rmrAG.Bool("from_me")
  142. evt.SenderID = rmrAG.OptionalJIDOrEmpty("participant")
  143. if !rmrAG.OK() {
  144. return nil, fmt.Errorf("missing attributes in <rmr> tag: %w", rmrAG.Error())
  145. }
  146. errNode, ok := node.GetOptionalChildByTag("error")
  147. if ok {
  148. evt.Error = &events.MediaRetryError{
  149. Code: errNode.AttrGetter().Int("code"),
  150. }
  151. return &evt, nil
  152. }
  153. evt.Ciphertext, ok = node.GetChildByTag("encrypt", "enc_p").Content.([]byte)
  154. if !ok {
  155. return nil, &ElementMissingError{Tag: "enc_p", In: fmt.Sprintf("retry notification %s", evt.MessageID)}
  156. }
  157. evt.IV, ok = node.GetChildByTag("encrypt", "enc_iv").Content.([]byte)
  158. if !ok {
  159. return nil, &ElementMissingError{Tag: "enc_iv", In: fmt.Sprintf("retry notification %s", evt.MessageID)}
  160. }
  161. return &evt, nil
  162. }
  163. func (cli *Client) handleMediaRetryNotification(ctx context.Context, node *waBinary.Node) {
  164. evt, err := parseMediaRetryNotification(node)
  165. if err != nil {
  166. cli.Log.Warnf("Failed to parse media retry notification: %v", err)
  167. return
  168. }
  169. cli.dispatchEvent(evt)
  170. }