encode.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. package appstate
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "encoding/json"
  6. "fmt"
  7. "time"
  8. "google.golang.org/protobuf/proto"
  9. "go.mau.fi/whatsmeow/proto/waCommon"
  10. "go.mau.fi/whatsmeow/proto/waServerSync"
  11. "go.mau.fi/whatsmeow/proto/waSyncAction"
  12. "go.mau.fi/whatsmeow/types"
  13. "go.mau.fi/whatsmeow/util/cbcutil"
  14. )
  15. // MutationInfo contains information about a single mutation to the app state.
  16. type MutationInfo struct {
  17. // Index contains the thing being mutated (like `mute` or `pin_v1`), followed by parameters like the target JID.
  18. Index []string
  19. // Version is a static number that depends on the thing being mutated.
  20. Version int32
  21. // Value contains the data for the mutation.
  22. Value *waSyncAction.SyncActionValue
  23. }
  24. // PatchInfo contains information about a patch to the app state.
  25. // A patch can contain multiple mutations, as long as all mutations are in the same app state type.
  26. type PatchInfo struct {
  27. // Timestamp is the time when the patch was created. This will be filled automatically in EncodePatch if it's zero.
  28. Timestamp time.Time
  29. // Type is the app state type being mutated.
  30. Type WAPatchName
  31. // Mutations contains the individual mutations to apply to the app state in this patch.
  32. Mutations []MutationInfo
  33. }
  34. // BuildMute builds an app state patch for muting or unmuting a chat.
  35. //
  36. // If mute is true and the mute duration is zero, the chat is muted forever.
  37. func BuildMute(target types.JID, mute bool, muteDuration time.Duration) PatchInfo {
  38. var muteEndTimestamp *int64
  39. if muteDuration > 0 {
  40. muteEndTimestamp = proto.Int64(time.Now().Add(muteDuration).UnixMilli())
  41. }
  42. return BuildMuteAbs(target, mute, muteEndTimestamp)
  43. }
  44. // BuildMuteAbs builds an app state patch for muting or unmuting a chat with an absolute timestamp.
  45. func BuildMuteAbs(target types.JID, mute bool, muteEndTimestamp *int64) PatchInfo {
  46. if muteEndTimestamp == nil && mute {
  47. muteEndTimestamp = proto.Int64(-1)
  48. }
  49. return PatchInfo{
  50. Type: WAPatchRegularHigh,
  51. Mutations: []MutationInfo{{
  52. Index: []string{IndexMute, target.String()},
  53. Version: 2,
  54. Value: &waSyncAction.SyncActionValue{
  55. MuteAction: &waSyncAction.MuteAction{
  56. Muted: proto.Bool(mute),
  57. MuteEndTimestamp: muteEndTimestamp,
  58. },
  59. },
  60. }},
  61. }
  62. }
  63. func newPinMutationInfo(target types.JID, pin bool) MutationInfo {
  64. return MutationInfo{
  65. Index: []string{IndexPin, target.String()},
  66. Version: 5,
  67. Value: &waSyncAction.SyncActionValue{
  68. PinAction: &waSyncAction.PinAction{
  69. Pinned: &pin,
  70. },
  71. },
  72. }
  73. }
  74. // BuildPin builds an app state patch for pinning or unpinning a chat.
  75. func BuildPin(target types.JID, pin bool) PatchInfo {
  76. return PatchInfo{
  77. Type: WAPatchRegularLow,
  78. Mutations: []MutationInfo{
  79. newPinMutationInfo(target, pin),
  80. },
  81. }
  82. }
  83. // BuildArchive builds an app state patch for archiving or unarchiving a chat.
  84. //
  85. // The last message timestamp and last message key are optional and can be set to zero values (`time.Time{}` and `nil`).
  86. //
  87. // Archiving a chat will also unpin it automatically.
  88. func BuildArchive(target types.JID, archive bool, lastMessageTimestamp time.Time, lastMessageKey *waCommon.MessageKey) PatchInfo {
  89. archiveMutationInfo := MutationInfo{
  90. Index: []string{IndexArchive, target.String()},
  91. Version: 3,
  92. Value: &waSyncAction.SyncActionValue{
  93. ArchiveChatAction: &waSyncAction.ArchiveChatAction{
  94. Archived: &archive,
  95. MessageRange: newMessageRange(lastMessageTimestamp, lastMessageKey),
  96. // TODO set LastSystemMessageTimestamp?
  97. },
  98. },
  99. }
  100. mutations := []MutationInfo{archiveMutationInfo}
  101. if archive {
  102. mutations = append(mutations, newPinMutationInfo(target, false))
  103. }
  104. result := PatchInfo{
  105. Type: WAPatchRegularLow,
  106. Mutations: mutations,
  107. }
  108. return result
  109. }
  110. // BuildMarkChatAsRead builds an app state patch for marking a chat as read or unread.
  111. func BuildMarkChatAsRead(target types.JID, read bool, lastMessageTimestamp time.Time, lastMessageKey *waCommon.MessageKey) PatchInfo {
  112. action := &waSyncAction.MarkChatAsReadAction{
  113. Read: proto.Bool(read),
  114. MessageRange: newMessageRange(lastMessageTimestamp, lastMessageKey),
  115. }
  116. return PatchInfo{
  117. Type: WAPatchRegularLow,
  118. Mutations: []MutationInfo{{
  119. Index: []string{IndexMarkChatAsRead, target.String()},
  120. Version: 3,
  121. Value: &waSyncAction.SyncActionValue{
  122. MarkChatAsReadAction: action,
  123. },
  124. }},
  125. }
  126. }
  127. func newLabelChatMutation(target types.JID, labelID string, labeled bool) MutationInfo {
  128. return MutationInfo{
  129. Index: []string{IndexLabelAssociationChat, labelID, target.String()},
  130. Version: 3,
  131. Value: &waSyncAction.SyncActionValue{
  132. LabelAssociationAction: &waSyncAction.LabelAssociationAction{
  133. Labeled: &labeled,
  134. },
  135. },
  136. }
  137. }
  138. // BuildLabelChat builds an app state patch for labeling or un(labeling) a chat.
  139. func BuildLabelChat(target types.JID, labelID string, labeled bool) PatchInfo {
  140. return PatchInfo{
  141. Type: WAPatchRegular,
  142. Mutations: []MutationInfo{
  143. newLabelChatMutation(target, labelID, labeled),
  144. },
  145. }
  146. }
  147. func newLabelMessageMutation(target types.JID, labelID, messageID string, labeled bool) MutationInfo {
  148. return MutationInfo{
  149. Index: []string{IndexLabelAssociationMessage, labelID, target.String(), messageID, "0", "0"},
  150. Version: 3,
  151. Value: &waSyncAction.SyncActionValue{
  152. LabelAssociationAction: &waSyncAction.LabelAssociationAction{
  153. Labeled: &labeled,
  154. },
  155. },
  156. }
  157. }
  158. // BuildLabelMessage builds an app state patch for labeling or un(labeling) a message.
  159. func BuildLabelMessage(target types.JID, labelID, messageID string, labeled bool) PatchInfo {
  160. return PatchInfo{
  161. Type: WAPatchRegular,
  162. Mutations: []MutationInfo{
  163. newLabelMessageMutation(target, labelID, messageID, labeled),
  164. },
  165. }
  166. }
  167. func newLabelEditMutation(labelID string, labelName string, labelColor int32, deleted bool) MutationInfo {
  168. return MutationInfo{
  169. Index: []string{IndexLabelEdit, labelID},
  170. Version: 3,
  171. Value: &waSyncAction.SyncActionValue{
  172. LabelEditAction: &waSyncAction.LabelEditAction{
  173. Name: &labelName,
  174. Color: &labelColor,
  175. Deleted: &deleted,
  176. },
  177. },
  178. }
  179. }
  180. // BuildLabelEdit builds an app state patch for editing a label.
  181. func BuildLabelEdit(labelID string, labelName string, labelColor int32, deleted bool) PatchInfo {
  182. return PatchInfo{
  183. Type: WAPatchRegular,
  184. Mutations: []MutationInfo{
  185. newLabelEditMutation(labelID, labelName, labelColor, deleted),
  186. },
  187. }
  188. }
  189. func newSettingPushNameMutation(pushName string) MutationInfo {
  190. return MutationInfo{
  191. Index: []string{IndexSettingPushName},
  192. Version: 1,
  193. Value: &waSyncAction.SyncActionValue{
  194. PushNameSetting: &waSyncAction.PushNameSetting{
  195. Name: &pushName,
  196. },
  197. },
  198. }
  199. }
  200. // BuildSettingPushName builds an app state patch for setting the push name.
  201. func BuildSettingPushName(pushName string) PatchInfo {
  202. return PatchInfo{
  203. Type: WAPatchCriticalBlock,
  204. Mutations: []MutationInfo{
  205. newSettingPushNameMutation(pushName),
  206. },
  207. }
  208. }
  209. func newStarMutation(targetJID, senderJID string, messageID types.MessageID, fromMe string, starred bool) MutationInfo {
  210. return MutationInfo{
  211. Index: []string{IndexStar, targetJID, messageID, fromMe, senderJID},
  212. Version: 2,
  213. Value: &waSyncAction.SyncActionValue{
  214. StarAction: &waSyncAction.StarAction{
  215. Starred: &starred,
  216. },
  217. },
  218. }
  219. }
  220. // BuildStar builds an app state patch for starring or unstarring a message.
  221. func BuildStar(target, sender types.JID, messageID types.MessageID, fromMe, starred bool) PatchInfo {
  222. isFromMe := "0"
  223. if fromMe {
  224. isFromMe = "1"
  225. }
  226. targetJID, senderJID := target.String(), sender.String()
  227. if target.User == sender.User {
  228. senderJID = "0"
  229. }
  230. return PatchInfo{
  231. Type: WAPatchRegularHigh,
  232. Mutations: []MutationInfo{
  233. newStarMutation(targetJID, senderJID, messageID, isFromMe, starred),
  234. },
  235. }
  236. }
  237. func (proc *Processor) EncodePatch(ctx context.Context, keyID []byte, state HashState, patchInfo PatchInfo) ([]byte, error) {
  238. keys, err := proc.getAppStateKey(ctx, keyID)
  239. if err != nil {
  240. return nil, fmt.Errorf("failed to get app state key details with key ID %x: %w", keyID, err)
  241. }
  242. if patchInfo.Timestamp.IsZero() {
  243. patchInfo.Timestamp = time.Now()
  244. }
  245. mutations := make([]*waServerSync.SyncdMutation, 0, len(patchInfo.Mutations))
  246. for _, mutationInfo := range patchInfo.Mutations {
  247. mutationInfo.Value.Timestamp = proto.Int64(patchInfo.Timestamp.UnixMilli())
  248. indexBytes, err := json.Marshal(mutationInfo.Index)
  249. if err != nil {
  250. return nil, fmt.Errorf("failed to marshal mutation index: %w", err)
  251. }
  252. pbObj := &waSyncAction.SyncActionData{
  253. Index: indexBytes,
  254. Value: mutationInfo.Value,
  255. Padding: []byte{},
  256. Version: &mutationInfo.Version,
  257. }
  258. content, err := proto.Marshal(pbObj)
  259. if err != nil {
  260. return nil, fmt.Errorf("failed to marshal mutation: %w", err)
  261. }
  262. encryptedContent, err := cbcutil.Encrypt(keys.ValueEncryption, nil, content)
  263. if err != nil {
  264. return nil, fmt.Errorf("failed to encrypt mutation: %w", err)
  265. }
  266. valueMac := generateContentMAC(waServerSync.SyncdMutation_SET, encryptedContent, keyID, keys.ValueMAC)
  267. indexMac := concatAndHMAC(sha256.New, keys.Index, indexBytes)
  268. mutations = append(mutations, &waServerSync.SyncdMutation{
  269. Operation: waServerSync.SyncdMutation_SET.Enum(),
  270. Record: &waServerSync.SyncdRecord{
  271. Index: &waServerSync.SyncdIndex{Blob: indexMac},
  272. Value: &waServerSync.SyncdValue{Blob: append(encryptedContent, valueMac...)},
  273. KeyID: &waServerSync.KeyId{ID: keyID},
  274. },
  275. })
  276. }
  277. warn, err := state.updateHash(mutations, func(indexMAC []byte, _ int) ([]byte, error) {
  278. return proc.Store.AppState.GetAppStateMutationMAC(ctx, string(patchInfo.Type), indexMAC)
  279. })
  280. if len(warn) > 0 {
  281. proc.Log.Warnf("Warnings while updating hash for %s (sending new app state): %+v", patchInfo.Type, warn)
  282. }
  283. if err != nil {
  284. return nil, fmt.Errorf("failed to update state hash: %w", err)
  285. }
  286. state.Version += 1
  287. syncdPatch := &waServerSync.SyncdPatch{
  288. SnapshotMAC: state.generateSnapshotMAC(patchInfo.Type, keys.SnapshotMAC),
  289. KeyID: &waServerSync.KeyId{ID: keyID},
  290. Mutations: mutations,
  291. }
  292. syncdPatch.PatchMAC = generatePatchMAC(syncdPatch, patchInfo.Type, keys.PatchMAC, state.Version)
  293. result, err := proto.Marshal(syncdPatch)
  294. if err != nil {
  295. return nil, fmt.Errorf("failed to marshal compiled patch: %w", err)
  296. }
  297. return result, nil
  298. }
  299. // BuildDeleteChat builds an app state patch for deleting a chat.
  300. func BuildDeleteChat(target types.JID, lastMessageTimestamp time.Time, lastMessageKey *waCommon.MessageKey) PatchInfo {
  301. action := &waSyncAction.DeleteChatAction{
  302. MessageRange: newMessageRange(lastMessageTimestamp, lastMessageKey),
  303. }
  304. return PatchInfo{
  305. Type: WAPatchRegular,
  306. Mutations: []MutationInfo{{
  307. Index: []string{IndexDeleteChat, target.String()},
  308. Version: 6,
  309. Value: &waSyncAction.SyncActionValue{
  310. DeleteChatAction: action,
  311. },
  312. }},
  313. }
  314. }
  315. func newMessageRange(lastMessageTimestamp time.Time, lastMessageKey *waCommon.MessageKey) *waSyncAction.SyncActionMessageRange {
  316. if lastMessageTimestamp.IsZero() {
  317. lastMessageTimestamp = time.Now()
  318. }
  319. messageRange := &waSyncAction.SyncActionMessageRange{
  320. LastMessageTimestamp: proto.Int64(lastMessageTimestamp.Unix()),
  321. }
  322. if lastMessageKey != nil {
  323. messageRange.Messages = []*waSyncAction.SyncActionMessage{{
  324. Key: lastMessageKey,
  325. Timestamp: proto.Int64(lastMessageTimestamp.Unix()),
  326. }}
  327. }
  328. return messageRange
  329. }