newsletter.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. // Copyright (c) 2023 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. "encoding/json"
  10. "fmt"
  11. "log"
  12. "strings"
  13. "time"
  14. "github.com/beeper/argo-go/codec"
  15. "github.com/beeper/argo-go/pkg/buf"
  16. "git.bobomao.top/joey/testwh/argo"
  17. waBinary "git.bobomao.top/joey/testwh/binary"
  18. "git.bobomao.top/joey/testwh/proto/waWa6"
  19. "git.bobomao.top/joey/testwh/store"
  20. "git.bobomao.top/joey/testwh/types"
  21. )
  22. // NewsletterSubscribeLiveUpdates subscribes to receive live updates from a WhatsApp channel temporarily (for the duration returned).
  23. func (cli *Client) NewsletterSubscribeLiveUpdates(ctx context.Context, jid types.JID) (time.Duration, error) {
  24. resp, err := cli.sendIQ(ctx, infoQuery{
  25. Namespace: "newsletter",
  26. Type: iqSet,
  27. To: jid,
  28. Content: []waBinary.Node{{
  29. Tag: "live_updates",
  30. }},
  31. })
  32. if err != nil {
  33. return 0, err
  34. }
  35. child := resp.GetChildByTag("live_updates")
  36. dur := child.AttrGetter().Int("duration")
  37. return time.Duration(dur) * time.Second, nil
  38. }
  39. // NewsletterMarkViewed marks a channel message as viewed, incrementing the view counter.
  40. //
  41. // This is not the same as marking the channel as read on your other devices, use the usual MarkRead function for that.
  42. func (cli *Client) NewsletterMarkViewed(ctx context.Context, jid types.JID, serverIDs []types.MessageServerID) error {
  43. if cli == nil {
  44. return ErrClientIsNil
  45. }
  46. items := make([]waBinary.Node, len(serverIDs))
  47. for i, id := range serverIDs {
  48. items[i] = waBinary.Node{
  49. Tag: "item",
  50. Attrs: waBinary.Attrs{
  51. "server_id": id,
  52. },
  53. }
  54. }
  55. reqID := cli.generateRequestID()
  56. resp := cli.waitResponse(reqID)
  57. err := cli.sendNode(ctx, waBinary.Node{
  58. Tag: "receipt",
  59. Attrs: waBinary.Attrs{
  60. "to": jid,
  61. "type": "view",
  62. "id": reqID,
  63. },
  64. Content: []waBinary.Node{{
  65. Tag: "list",
  66. Content: items,
  67. }},
  68. })
  69. if err != nil {
  70. cli.cancelResponse(reqID, resp)
  71. return err
  72. }
  73. // TODO handle response?
  74. <-resp
  75. return nil
  76. }
  77. // NewsletterSendReaction sends a reaction to a channel message.
  78. // To remove a reaction sent earlier, set reaction to an empty string.
  79. //
  80. // The last parameter is the message ID of the reaction itself. It can be left empty to let whatsmeow generate a random one.
  81. func (cli *Client) NewsletterSendReaction(ctx context.Context, jid types.JID, serverID types.MessageServerID, reaction string, messageID types.MessageID) error {
  82. if messageID == "" {
  83. messageID = cli.GenerateMessageID()
  84. }
  85. reactionAttrs := waBinary.Attrs{}
  86. messageAttrs := waBinary.Attrs{
  87. "to": jid,
  88. "id": messageID,
  89. "server_id": serverID,
  90. "type": "reaction",
  91. }
  92. if reaction != "" {
  93. reactionAttrs["code"] = reaction
  94. } else {
  95. messageAttrs["edit"] = string(types.EditAttributeSenderRevoke)
  96. }
  97. return cli.sendNode(ctx, waBinary.Node{
  98. Tag: "message",
  99. Attrs: messageAttrs,
  100. Content: []waBinary.Node{{
  101. Tag: "reaction",
  102. Attrs: reactionAttrs,
  103. }},
  104. })
  105. }
  106. const (
  107. queryFetchNewsletter = "6563316087068696"
  108. queryFetchNewsletterDehydrated = "7272540469429201"
  109. queryRecommendedNewsletters = "7263823273662354" // variables -> input -> {limit: 20, country_codes: [string]}, output: xwa2_newsletters_recommended
  110. queryNewslettersDirectory = "6190824427689257" // variables -> input -> {view: "RECOMMENDED", limit: 50, start_cursor: base64, filters: {country_codes: [string]}}
  111. querySubscribedNewsletters = "6388546374527196" // variables -> empty, output: xwa2_newsletter_subscribed
  112. queryNewsletterSubscribers = "9800646650009898" // variables -> input -> {newsletter_id, count}, output: xwa2_newsletter_subscribers -> subscribers -> edges
  113. mutationMuteNewsletter = "6274038279359549" // variables -> {newsletter_id, updates->{description, settings}}, output: xwa2_newsletter_update -> NewsletterMetadata without viewer meta
  114. mutationUnmuteNewsletter = "6068417879924485"
  115. mutationUpdateNewsletter = "7150902998257522"
  116. mutationCreateNewsletter = "6234210096708695"
  117. mutationUnfollowNewsletter = "6392786840836363"
  118. mutationFollowNewsletter = "9926858900719341"
  119. // desktop & mobile
  120. queryFetchNewsletterDesktop = "9779843322044422"
  121. queryRecommendedNewslettersDesktop = "27256776790637714"
  122. querySubscribedNewslettersDesktop = "8621797084555037"
  123. queryNewsletterSubscribersDesktop = "25403502652570342"
  124. mutationMuteNewsletterDesktop = "5971669009605755" // variables -> {newsletter_id, updates->{description, settings}}, output: xwa2_newsletter_update -> NewsletterMetadata without viewer meta
  125. mutationUnmuteNewsletterDesktop = "6104029483058502"
  126. mutationUpdateNewsletterDesktop = "7839742399440946"
  127. mutationCreateNewsletterDesktop = "27527996220149684"
  128. mutationUnfollowNewsletterDesktop = "8782612271820087"
  129. mutationFollowNewsletterDesktop = "8621797084555037"
  130. )
  131. func convertQueryID(cli *Client, queryID string) string {
  132. if payload := cli.Store.GetClientPayload(); payload.GetUserAgent().Platform == waWa6.ClientPayload_UserAgent_MACOS.Enum() || payload.GetWebInfo() == nil {
  133. switch queryID {
  134. case queryFetchNewsletter:
  135. return queryFetchNewsletterDesktop
  136. case queryRecommendedNewsletters:
  137. return queryRecommendedNewslettersDesktop
  138. case querySubscribedNewsletters:
  139. return querySubscribedNewslettersDesktop
  140. case queryNewsletterSubscribers:
  141. return queryNewsletterSubscribersDesktop
  142. case mutationMuteNewsletter:
  143. return mutationMuteNewsletterDesktop
  144. case mutationUnmuteNewsletter:
  145. return mutationUnmuteNewsletterDesktop
  146. case mutationUpdateNewsletter:
  147. return mutationUpdateNewsletterDesktop
  148. case mutationCreateNewsletter:
  149. return mutationCreateNewsletterDesktop
  150. case mutationUnfollowNewsletter:
  151. return mutationUnfollowNewsletterDesktop
  152. case mutationFollowNewsletter:
  153. return mutationFollowNewsletterDesktop
  154. default:
  155. return queryID
  156. }
  157. } else {
  158. return queryID
  159. }
  160. }
  161. func (cli *Client) sendMexIQ(ctx context.Context, queryID string, variables any) (json.RawMessage, error) {
  162. if store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS {
  163. return nil, fmt.Errorf("argo decoding is currently broken")
  164. }
  165. queryID = convertQueryID(cli, queryID)
  166. payload, err := json.Marshal(map[string]any{
  167. "variables": variables,
  168. })
  169. if err != nil {
  170. return nil, err
  171. }
  172. resp, err := cli.sendIQ(ctx, infoQuery{
  173. Namespace: "w:mex",
  174. Type: iqGet,
  175. To: types.ServerJID,
  176. Content: []waBinary.Node{{
  177. Tag: "query",
  178. Attrs: waBinary.Attrs{
  179. "query_id": queryID,
  180. },
  181. Content: payload,
  182. }},
  183. })
  184. if err != nil {
  185. return nil, err
  186. }
  187. result, ok := resp.GetOptionalChildByTag("result")
  188. if !ok {
  189. return nil, &ElementMissingError{Tag: "result", In: "mex response"}
  190. }
  191. resultContent, ok := result.Content.([]byte)
  192. if !ok {
  193. return nil, fmt.Errorf("unexpected content type %T in mex response", result.Content)
  194. }
  195. if result.AttrGetter().OptionalString("format") == "argo" {
  196. if true {
  197. return nil, fmt.Errorf("argo decoding is currently broken")
  198. }
  199. store, err := argo.GetStore()
  200. if err != nil {
  201. return nil, err
  202. }
  203. queryIDMap, err := argo.GetQueryIDToMessageName()
  204. if err != nil {
  205. return nil, err
  206. }
  207. wt := store[queryIDMap[queryID]]
  208. decoder, err := codec.NewArgoDecoder(buf.NewBufReadonly(resultContent))
  209. if err != nil {
  210. return nil, err
  211. }
  212. data, err := decoder.ArgoToMap(wt)
  213. if err != nil {
  214. log.Fatalf("argo to map error: %v", err)
  215. }
  216. b, err := json.Marshal(data)
  217. if err != nil {
  218. return nil, err
  219. }
  220. return b, nil
  221. } else {
  222. var gqlResp types.GraphQLResponse
  223. err = json.Unmarshal(resultContent, &gqlResp)
  224. if err != nil {
  225. return nil, fmt.Errorf("failed to unmarshal graphql response: %w", err)
  226. } else if len(gqlResp.Errors) > 0 {
  227. return gqlResp.Data, fmt.Errorf("graphql error: %w", gqlResp.Errors)
  228. }
  229. return gqlResp.Data, nil
  230. }
  231. }
  232. type respGetNewsletterInfo struct {
  233. Newsletter *types.NewsletterMetadata `json:"xwa2_newsletter"`
  234. }
  235. func (cli *Client) getNewsletterInfo(ctx context.Context, input map[string]any, fetchViewerMeta bool) (*types.NewsletterMetadata, error) {
  236. data, err := cli.sendMexIQ(ctx, queryFetchNewsletter, map[string]any{
  237. "fetch_creation_time": true,
  238. "fetch_full_image": true,
  239. "fetch_viewer_metadata": fetchViewerMeta,
  240. "input": input,
  241. })
  242. var respData respGetNewsletterInfo
  243. if data != nil {
  244. jsonErr := json.Unmarshal(data, &respData)
  245. if err == nil && jsonErr != nil {
  246. err = jsonErr
  247. }
  248. }
  249. return respData.Newsletter, err
  250. }
  251. // GetNewsletterInfo gets the info of a newsletter that you're joined to.
  252. func (cli *Client) GetNewsletterInfo(ctx context.Context, jid types.JID) (*types.NewsletterMetadata, error) {
  253. return cli.getNewsletterInfo(ctx, map[string]any{
  254. "key": jid.String(),
  255. "type": types.NewsletterKeyTypeJID,
  256. }, true)
  257. }
  258. // GetNewsletterInfoWithInvite gets the info of a newsletter with an invite link.
  259. //
  260. // You can either pass the full link (https://whatsapp.com/channel/...) or just the `...` part.
  261. //
  262. // Note that the ViewerMeta field of the returned NewsletterMetadata will be nil.
  263. func (cli *Client) GetNewsletterInfoWithInvite(ctx context.Context, key string) (*types.NewsletterMetadata, error) {
  264. return cli.getNewsletterInfo(ctx, map[string]any{
  265. "key": strings.TrimPrefix(key, NewsletterLinkPrefix),
  266. "type": types.NewsletterKeyTypeInvite,
  267. }, false)
  268. }
  269. type respGetSubscribedNewsletters struct {
  270. Newsletters []*types.NewsletterMetadata `json:"xwa2_newsletter_subscribed"`
  271. }
  272. // GetSubscribedNewsletters gets the info of all newsletters that you're joined to.
  273. func (cli *Client) GetSubscribedNewsletters(ctx context.Context) ([]*types.NewsletterMetadata, error) {
  274. data, err := cli.sendMexIQ(ctx, querySubscribedNewsletters, map[string]any{})
  275. var respData respGetSubscribedNewsletters
  276. if data != nil {
  277. jsonErr := json.Unmarshal(data, &respData)
  278. if err == nil && jsonErr != nil {
  279. err = jsonErr
  280. }
  281. }
  282. return respData.Newsletters, err
  283. }
  284. type CreateNewsletterParams struct {
  285. Name string `json:"name"`
  286. Description string `json:"description,omitempty"`
  287. Picture []byte `json:"picture,omitempty"`
  288. }
  289. type respCreateNewsletter struct {
  290. Newsletter *types.NewsletterMetadata `json:"xwa2_newsletter_create"`
  291. }
  292. // CreateNewsletter creates a new WhatsApp channel.
  293. func (cli *Client) CreateNewsletter(ctx context.Context, params CreateNewsletterParams) (*types.NewsletterMetadata, error) {
  294. resp, err := cli.sendMexIQ(ctx, mutationCreateNewsletter, map[string]any{
  295. "newsletter_input": &params,
  296. })
  297. if err != nil {
  298. return nil, err
  299. }
  300. var respData respCreateNewsletter
  301. err = json.Unmarshal(resp, &respData)
  302. if err != nil {
  303. return nil, err
  304. }
  305. return respData.Newsletter, nil
  306. }
  307. // AcceptTOSNotice accepts a ToS notice.
  308. //
  309. // To accept the terms for creating newsletters, use
  310. //
  311. // cli.AcceptTOSNotice("20601218", "5")
  312. func (cli *Client) AcceptTOSNotice(ctx context.Context, noticeID, stage string) error {
  313. _, err := cli.sendIQ(ctx, infoQuery{
  314. Namespace: "tos",
  315. Type: iqSet,
  316. To: types.ServerJID,
  317. Content: []waBinary.Node{{
  318. Tag: "notice",
  319. Attrs: waBinary.Attrs{
  320. "id": noticeID,
  321. "stage": stage,
  322. },
  323. }},
  324. })
  325. return err
  326. }
  327. // NewsletterToggleMute changes the mute status of a newsletter.
  328. func (cli *Client) NewsletterToggleMute(ctx context.Context, jid types.JID, mute bool) error {
  329. query := mutationUnmuteNewsletter
  330. if mute {
  331. query = mutationMuteNewsletter
  332. }
  333. _, err := cli.sendMexIQ(ctx, query, map[string]any{
  334. "newsletter_id": jid.String(),
  335. })
  336. return err
  337. }
  338. // FollowNewsletter makes the user follow (join) a WhatsApp channel.
  339. func (cli *Client) FollowNewsletter(ctx context.Context, jid types.JID) error {
  340. _, err := cli.sendMexIQ(ctx, mutationFollowNewsletter, map[string]any{
  341. "newsletter_id": jid.String(),
  342. })
  343. return err
  344. }
  345. // UnfollowNewsletter makes the user unfollow (leave) a WhatsApp channel.
  346. func (cli *Client) UnfollowNewsletter(ctx context.Context, jid types.JID) error {
  347. _, err := cli.sendMexIQ(ctx, mutationUnfollowNewsletter, map[string]any{
  348. "newsletter_id": jid.String(),
  349. })
  350. return err
  351. }
  352. type GetNewsletterMessagesParams struct {
  353. Count int
  354. Before types.MessageServerID
  355. }
  356. // GetNewsletterMessages gets messages in a WhatsApp channel.
  357. func (cli *Client) GetNewsletterMessages(ctx context.Context, jid types.JID, params *GetNewsletterMessagesParams) ([]*types.NewsletterMessage, error) {
  358. attrs := waBinary.Attrs{
  359. "type": "jid",
  360. "jid": jid,
  361. }
  362. if params != nil {
  363. if params.Count != 0 {
  364. attrs["count"] = params.Count
  365. }
  366. if params.Before != 0 {
  367. attrs["before"] = params.Before
  368. }
  369. }
  370. resp, err := cli.sendIQ(ctx, infoQuery{
  371. Namespace: "newsletter",
  372. Type: iqGet,
  373. To: types.ServerJID,
  374. Content: []waBinary.Node{{
  375. Tag: "messages",
  376. Attrs: attrs,
  377. }},
  378. })
  379. if err != nil {
  380. return nil, err
  381. }
  382. messages, ok := resp.GetOptionalChildByTag("messages")
  383. if !ok {
  384. return nil, &ElementMissingError{Tag: "messages", In: "newsletter messages response"}
  385. }
  386. return cli.parseNewsletterMessages(&messages), nil
  387. }
  388. type GetNewsletterUpdatesParams struct {
  389. Count int
  390. Since time.Time
  391. After types.MessageServerID
  392. }
  393. // GetNewsletterMessageUpdates gets updates in a WhatsApp channel.
  394. //
  395. // These are the same kind of updates that NewsletterSubscribeLiveUpdates triggers (reaction and view counts).
  396. func (cli *Client) GetNewsletterMessageUpdates(ctx context.Context, jid types.JID, params *GetNewsletterUpdatesParams) ([]*types.NewsletterMessage, error) {
  397. attrs := waBinary.Attrs{}
  398. if params != nil {
  399. if params.Count != 0 {
  400. attrs["count"] = params.Count
  401. }
  402. if !params.Since.IsZero() {
  403. attrs["since"] = params.Since.Unix()
  404. }
  405. if params.After != 0 {
  406. attrs["after"] = params.After
  407. }
  408. }
  409. resp, err := cli.sendIQ(ctx, infoQuery{
  410. Namespace: "newsletter",
  411. Type: iqGet,
  412. To: jid,
  413. Content: []waBinary.Node{{
  414. Tag: "message_updates",
  415. Attrs: attrs,
  416. }},
  417. })
  418. if err != nil {
  419. return nil, err
  420. }
  421. messages, ok := resp.GetOptionalChildByTag("message_updates", "messages")
  422. if !ok {
  423. return nil, &ElementMissingError{Tag: "messages", In: "newsletter messages response"}
  424. }
  425. return cli.parseNewsletterMessages(&messages), nil
  426. }