| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- // Copyright (c) 2023 Tulir Asokan
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this
- // file, You can obtain one at http://mozilla.org/MPL/2.0/.
- package whatsmeow
- import (
- "context"
- "encoding/json"
- "fmt"
- "log"
- "strings"
- "time"
- "github.com/beeper/argo-go/codec"
- "github.com/beeper/argo-go/pkg/buf"
- "go.mau.fi/whatsmeow/argo"
- waBinary "go.mau.fi/whatsmeow/binary"
- "go.mau.fi/whatsmeow/proto/waWa6"
- "go.mau.fi/whatsmeow/store"
- "go.mau.fi/whatsmeow/types"
- )
- // NewsletterSubscribeLiveUpdates subscribes to receive live updates from a WhatsApp channel temporarily (for the duration returned).
- func (cli *Client) NewsletterSubscribeLiveUpdates(ctx context.Context, jid types.JID) (time.Duration, error) {
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "newsletter",
- Type: iqSet,
- To: jid,
- Content: []waBinary.Node{{
- Tag: "live_updates",
- }},
- })
- if err != nil {
- return 0, err
- }
- child := resp.GetChildByTag("live_updates")
- dur := child.AttrGetter().Int("duration")
- return time.Duration(dur) * time.Second, nil
- }
- // NewsletterMarkViewed marks a channel message as viewed, incrementing the view counter.
- //
- // This is not the same as marking the channel as read on your other devices, use the usual MarkRead function for that.
- func (cli *Client) NewsletterMarkViewed(ctx context.Context, jid types.JID, serverIDs []types.MessageServerID) error {
- if cli == nil {
- return ErrClientIsNil
- }
- items := make([]waBinary.Node, len(serverIDs))
- for i, id := range serverIDs {
- items[i] = waBinary.Node{
- Tag: "item",
- Attrs: waBinary.Attrs{
- "server_id": id,
- },
- }
- }
- reqID := cli.generateRequestID()
- resp := cli.waitResponse(reqID)
- err := cli.sendNode(ctx, waBinary.Node{
- Tag: "receipt",
- Attrs: waBinary.Attrs{
- "to": jid,
- "type": "view",
- "id": reqID,
- },
- Content: []waBinary.Node{{
- Tag: "list",
- Content: items,
- }},
- })
- if err != nil {
- cli.cancelResponse(reqID, resp)
- return err
- }
- // TODO handle response?
- <-resp
- return nil
- }
- // NewsletterSendReaction sends a reaction to a channel message.
- // To remove a reaction sent earlier, set reaction to an empty string.
- //
- // The last parameter is the message ID of the reaction itself. It can be left empty to let whatsmeow generate a random one.
- func (cli *Client) NewsletterSendReaction(ctx context.Context, jid types.JID, serverID types.MessageServerID, reaction string, messageID types.MessageID) error {
- if messageID == "" {
- messageID = cli.GenerateMessageID()
- }
- reactionAttrs := waBinary.Attrs{}
- messageAttrs := waBinary.Attrs{
- "to": jid,
- "id": messageID,
- "server_id": serverID,
- "type": "reaction",
- }
- if reaction != "" {
- reactionAttrs["code"] = reaction
- } else {
- messageAttrs["edit"] = string(types.EditAttributeSenderRevoke)
- }
- return cli.sendNode(ctx, waBinary.Node{
- Tag: "message",
- Attrs: messageAttrs,
- Content: []waBinary.Node{{
- Tag: "reaction",
- Attrs: reactionAttrs,
- }},
- })
- }
- const (
- queryFetchNewsletter = "6563316087068696"
- queryFetchNewsletterDehydrated = "7272540469429201"
- queryRecommendedNewsletters = "7263823273662354" // variables -> input -> {limit: 20, country_codes: [string]}, output: xwa2_newsletters_recommended
- queryNewslettersDirectory = "6190824427689257" // variables -> input -> {view: "RECOMMENDED", limit: 50, start_cursor: base64, filters: {country_codes: [string]}}
- querySubscribedNewsletters = "6388546374527196" // variables -> empty, output: xwa2_newsletter_subscribed
- queryNewsletterSubscribers = "9800646650009898" // variables -> input -> {newsletter_id, count}, output: xwa2_newsletter_subscribers -> subscribers -> edges
- mutationMuteNewsletter = "6274038279359549" // variables -> {newsletter_id, updates->{description, settings}}, output: xwa2_newsletter_update -> NewsletterMetadata without viewer meta
- mutationUnmuteNewsletter = "6068417879924485"
- mutationUpdateNewsletter = "7150902998257522"
- mutationCreateNewsletter = "6234210096708695"
- mutationUnfollowNewsletter = "6392786840836363"
- mutationFollowNewsletter = "9926858900719341"
- // desktop & mobile
- queryFetchNewsletterDesktop = "9779843322044422"
- queryRecommendedNewslettersDesktop = "27256776790637714"
- querySubscribedNewslettersDesktop = "8621797084555037"
- queryNewsletterSubscribersDesktop = "25403502652570342"
- mutationMuteNewsletterDesktop = "5971669009605755" // variables -> {newsletter_id, updates->{description, settings}}, output: xwa2_newsletter_update -> NewsletterMetadata without viewer meta
- mutationUnmuteNewsletterDesktop = "6104029483058502"
- mutationUpdateNewsletterDesktop = "7839742399440946"
- mutationCreateNewsletterDesktop = "27527996220149684"
- mutationUnfollowNewsletterDesktop = "8782612271820087"
- mutationFollowNewsletterDesktop = "8621797084555037"
- )
- func convertQueryID(cli *Client, queryID string) string {
- if payload := cli.Store.GetClientPayload(); payload.GetUserAgent().Platform == waWa6.ClientPayload_UserAgent_MACOS.Enum() || payload.GetWebInfo() == nil {
- switch queryID {
- case queryFetchNewsletter:
- return queryFetchNewsletterDesktop
- case queryRecommendedNewsletters:
- return queryRecommendedNewslettersDesktop
- case querySubscribedNewsletters:
- return querySubscribedNewslettersDesktop
- case queryNewsletterSubscribers:
- return queryNewsletterSubscribersDesktop
- case mutationMuteNewsletter:
- return mutationMuteNewsletterDesktop
- case mutationUnmuteNewsletter:
- return mutationUnmuteNewsletterDesktop
- case mutationUpdateNewsletter:
- return mutationUpdateNewsletterDesktop
- case mutationCreateNewsletter:
- return mutationCreateNewsletterDesktop
- case mutationUnfollowNewsletter:
- return mutationUnfollowNewsletterDesktop
- case mutationFollowNewsletter:
- return mutationFollowNewsletterDesktop
- default:
- return queryID
- }
- } else {
- return queryID
- }
- }
- func (cli *Client) sendMexIQ(ctx context.Context, queryID string, variables any) (json.RawMessage, error) {
- if store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS {
- return nil, fmt.Errorf("argo decoding is currently broken")
- }
- queryID = convertQueryID(cli, queryID)
- payload, err := json.Marshal(map[string]any{
- "variables": variables,
- })
- if err != nil {
- return nil, err
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "w:mex",
- Type: iqGet,
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "query",
- Attrs: waBinary.Attrs{
- "query_id": queryID,
- },
- Content: payload,
- }},
- })
- if err != nil {
- return nil, err
- }
- result, ok := resp.GetOptionalChildByTag("result")
- if !ok {
- return nil, &ElementMissingError{Tag: "result", In: "mex response"}
- }
- resultContent, ok := result.Content.([]byte)
- if !ok {
- return nil, fmt.Errorf("unexpected content type %T in mex response", result.Content)
- }
- if result.AttrGetter().OptionalString("format") == "argo" {
- if true {
- return nil, fmt.Errorf("argo decoding is currently broken")
- }
- store, err := argo.GetStore()
- if err != nil {
- return nil, err
- }
- queryIDMap, err := argo.GetQueryIDToMessageName()
- if err != nil {
- return nil, err
- }
- wt := store[queryIDMap[queryID]]
- decoder, err := codec.NewArgoDecoder(buf.NewBufReadonly(resultContent))
- if err != nil {
- return nil, err
- }
- data, err := decoder.ArgoToMap(wt)
- if err != nil {
- log.Fatalf("argo to map error: %v", err)
- }
- b, err := json.Marshal(data)
- if err != nil {
- return nil, err
- }
- return b, nil
- } else {
- var gqlResp types.GraphQLResponse
- err = json.Unmarshal(resultContent, &gqlResp)
- if err != nil {
- return nil, fmt.Errorf("failed to unmarshal graphql response: %w", err)
- } else if len(gqlResp.Errors) > 0 {
- return gqlResp.Data, fmt.Errorf("graphql error: %w", gqlResp.Errors)
- }
- return gqlResp.Data, nil
- }
- }
- type respGetNewsletterInfo struct {
- Newsletter *types.NewsletterMetadata `json:"xwa2_newsletter"`
- }
- func (cli *Client) getNewsletterInfo(ctx context.Context, input map[string]any, fetchViewerMeta bool) (*types.NewsletterMetadata, error) {
- data, err := cli.sendMexIQ(ctx, queryFetchNewsletter, map[string]any{
- "fetch_creation_time": true,
- "fetch_full_image": true,
- "fetch_viewer_metadata": fetchViewerMeta,
- "input": input,
- })
- var respData respGetNewsletterInfo
- if data != nil {
- jsonErr := json.Unmarshal(data, &respData)
- if err == nil && jsonErr != nil {
- err = jsonErr
- }
- }
- return respData.Newsletter, err
- }
- // GetNewsletterInfo gets the info of a newsletter that you're joined to.
- func (cli *Client) GetNewsletterInfo(ctx context.Context, jid types.JID) (*types.NewsletterMetadata, error) {
- return cli.getNewsletterInfo(ctx, map[string]any{
- "key": jid.String(),
- "type": types.NewsletterKeyTypeJID,
- }, true)
- }
- // GetNewsletterInfoWithInvite gets the info of a newsletter with an invite link.
- //
- // You can either pass the full link (https://whatsapp.com/channel/...) or just the `...` part.
- //
- // Note that the ViewerMeta field of the returned NewsletterMetadata will be nil.
- func (cli *Client) GetNewsletterInfoWithInvite(ctx context.Context, key string) (*types.NewsletterMetadata, error) {
- return cli.getNewsletterInfo(ctx, map[string]any{
- "key": strings.TrimPrefix(key, NewsletterLinkPrefix),
- "type": types.NewsletterKeyTypeInvite,
- }, false)
- }
- type respGetSubscribedNewsletters struct {
- Newsletters []*types.NewsletterMetadata `json:"xwa2_newsletter_subscribed"`
- }
- // GetSubscribedNewsletters gets the info of all newsletters that you're joined to.
- func (cli *Client) GetSubscribedNewsletters(ctx context.Context) ([]*types.NewsletterMetadata, error) {
- data, err := cli.sendMexIQ(ctx, querySubscribedNewsletters, map[string]any{})
- var respData respGetSubscribedNewsletters
- if data != nil {
- jsonErr := json.Unmarshal(data, &respData)
- if err == nil && jsonErr != nil {
- err = jsonErr
- }
- }
- return respData.Newsletters, err
- }
- type CreateNewsletterParams struct {
- Name string `json:"name"`
- Description string `json:"description,omitempty"`
- Picture []byte `json:"picture,omitempty"`
- }
- type respCreateNewsletter struct {
- Newsletter *types.NewsletterMetadata `json:"xwa2_newsletter_create"`
- }
- // CreateNewsletter creates a new WhatsApp channel.
- func (cli *Client) CreateNewsletter(ctx context.Context, params CreateNewsletterParams) (*types.NewsletterMetadata, error) {
- resp, err := cli.sendMexIQ(ctx, mutationCreateNewsletter, map[string]any{
- "newsletter_input": ¶ms,
- })
- if err != nil {
- return nil, err
- }
- var respData respCreateNewsletter
- err = json.Unmarshal(resp, &respData)
- if err != nil {
- return nil, err
- }
- return respData.Newsletter, nil
- }
- // AcceptTOSNotice accepts a ToS notice.
- //
- // To accept the terms for creating newsletters, use
- //
- // cli.AcceptTOSNotice("20601218", "5")
- func (cli *Client) AcceptTOSNotice(ctx context.Context, noticeID, stage string) error {
- _, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "tos",
- Type: iqSet,
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "notice",
- Attrs: waBinary.Attrs{
- "id": noticeID,
- "stage": stage,
- },
- }},
- })
- return err
- }
- // NewsletterToggleMute changes the mute status of a newsletter.
- func (cli *Client) NewsletterToggleMute(ctx context.Context, jid types.JID, mute bool) error {
- query := mutationUnmuteNewsletter
- if mute {
- query = mutationMuteNewsletter
- }
- _, err := cli.sendMexIQ(ctx, query, map[string]any{
- "newsletter_id": jid.String(),
- })
- return err
- }
- // FollowNewsletter makes the user follow (join) a WhatsApp channel.
- func (cli *Client) FollowNewsletter(ctx context.Context, jid types.JID) error {
- _, err := cli.sendMexIQ(ctx, mutationFollowNewsletter, map[string]any{
- "newsletter_id": jid.String(),
- })
- return err
- }
- // UnfollowNewsletter makes the user unfollow (leave) a WhatsApp channel.
- func (cli *Client) UnfollowNewsletter(ctx context.Context, jid types.JID) error {
- _, err := cli.sendMexIQ(ctx, mutationUnfollowNewsletter, map[string]any{
- "newsletter_id": jid.String(),
- })
- return err
- }
- type GetNewsletterMessagesParams struct {
- Count int
- Before types.MessageServerID
- }
- // GetNewsletterMessages gets messages in a WhatsApp channel.
- func (cli *Client) GetNewsletterMessages(ctx context.Context, jid types.JID, params *GetNewsletterMessagesParams) ([]*types.NewsletterMessage, error) {
- attrs := waBinary.Attrs{
- "type": "jid",
- "jid": jid,
- }
- if params != nil {
- if params.Count != 0 {
- attrs["count"] = params.Count
- }
- if params.Before != 0 {
- attrs["before"] = params.Before
- }
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "newsletter",
- Type: iqGet,
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "messages",
- Attrs: attrs,
- }},
- })
- if err != nil {
- return nil, err
- }
- messages, ok := resp.GetOptionalChildByTag("messages")
- if !ok {
- return nil, &ElementMissingError{Tag: "messages", In: "newsletter messages response"}
- }
- return cli.parseNewsletterMessages(&messages), nil
- }
- type GetNewsletterUpdatesParams struct {
- Count int
- Since time.Time
- After types.MessageServerID
- }
- // GetNewsletterMessageUpdates gets updates in a WhatsApp channel.
- //
- // These are the same kind of updates that NewsletterSubscribeLiveUpdates triggers (reaction and view counts).
- func (cli *Client) GetNewsletterMessageUpdates(ctx context.Context, jid types.JID, params *GetNewsletterUpdatesParams) ([]*types.NewsletterMessage, error) {
- attrs := waBinary.Attrs{}
- if params != nil {
- if params.Count != 0 {
- attrs["count"] = params.Count
- }
- if !params.Since.IsZero() {
- attrs["since"] = params.Since.Unix()
- }
- if params.After != 0 {
- attrs["after"] = params.After
- }
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "newsletter",
- Type: iqGet,
- To: jid,
- Content: []waBinary.Node{{
- Tag: "message_updates",
- Attrs: attrs,
- }},
- })
- if err != nil {
- return nil, err
- }
- messages, ok := resp.GetOptionalChildByTag("message_updates", "messages")
- if !ok {
- return nil, &ElementMissingError{Tag: "messages", In: "newsletter messages response"}
- }
- return cli.parseNewsletterMessages(&messages), nil
- }
|