455 lines
14 KiB
Go
455 lines
14 KiB
Go
// 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"
|
|
"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": stripURLPrefix(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
|
|
}
|