summaryrefslogtreecommitdiffstats
path: root/vendor/go.mau.fi/whatsmeow/group.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/group.go')
-rw-r--r--vendor/go.mau.fi/whatsmeow/group.go566
1 files changed, 566 insertions, 0 deletions
diff --git a/vendor/go.mau.fi/whatsmeow/group.go b/vendor/go.mau.fi/whatsmeow/group.go
new file mode 100644
index 00000000..947b11b4
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/group.go
@@ -0,0 +1,566 @@
+// Copyright (c) 2021 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 (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ waBinary "go.mau.fi/whatsmeow/binary"
+ "go.mau.fi/whatsmeow/types"
+ "go.mau.fi/whatsmeow/types/events"
+)
+
+const InviteLinkPrefix = "https://chat.whatsapp.com/"
+
+func (cli *Client) sendGroupIQ(iqType infoQueryType, jid types.JID, content waBinary.Node) (*waBinary.Node, error) {
+ return cli.sendIQ(infoQuery{
+ Namespace: "w:g2",
+ Type: iqType,
+ To: jid,
+ Content: []waBinary.Node{content},
+ })
+}
+
+// CreateGroup creates a group on WhatsApp with the given name and participants.
+//
+// You don't need to include your own JID in the participants array, the WhatsApp servers will add it implicitly.
+func (cli *Client) CreateGroup(name string, participants []types.JID) (*types.GroupInfo, error) {
+ participantNodes := make([]waBinary.Node, len(participants))
+ for i, participant := range participants {
+ participantNodes[i] = waBinary.Node{
+ Tag: "participant",
+ Attrs: waBinary.Attrs{"jid": participant},
+ }
+ }
+ key := GenerateMessageID()
+ resp, err := cli.sendGroupIQ(iqSet, types.GroupServerJID, waBinary.Node{
+ Tag: "create",
+ Attrs: waBinary.Attrs{
+ "subject": name,
+ "key": key,
+ },
+ Content: participantNodes,
+ })
+ if err != nil {
+ return nil, err
+ }
+ groupNode, ok := resp.GetOptionalChildByTag("group")
+ if !ok {
+ return nil, &ElementMissingError{Tag: "group", In: "response to create group query"}
+ }
+ return cli.parseGroupNode(&groupNode)
+}
+
+// LeaveGroup leaves the specified group on WhatsApp.
+func (cli *Client) LeaveGroup(jid types.JID) error {
+ _, err := cli.sendGroupIQ(iqSet, types.GroupServerJID, waBinary.Node{
+ Tag: "leave",
+ Content: []waBinary.Node{{
+ Tag: "group",
+ Attrs: waBinary.Attrs{"id": jid},
+ }},
+ })
+ return err
+}
+
+type ParticipantChange string
+
+const (
+ ParticipantChangeAdd ParticipantChange = "add"
+ ParticipantChangeRemove ParticipantChange = "remove"
+ ParticipantChangePromote ParticipantChange = "promote"
+ ParticipantChangeDemote ParticipantChange = "demote"
+)
+
+// UpdateGroupParticipants can be used to add, remove, promote and demote members in a WhatsApp group.
+func (cli *Client) UpdateGroupParticipants(jid types.JID, participantChanges map[types.JID]ParticipantChange) (*waBinary.Node, error) {
+ content := make([]waBinary.Node, len(participantChanges))
+ i := 0
+ for participantJID, change := range participantChanges {
+ content[i] = waBinary.Node{
+ Tag: string(change),
+ Content: []waBinary.Node{{
+ Tag: "participant",
+ Attrs: waBinary.Attrs{"jid": participantJID},
+ }},
+ }
+ i++
+ }
+ resp, err := cli.sendIQ(infoQuery{
+ Namespace: "w:g2",
+ Type: iqSet,
+ To: jid,
+ Content: content,
+ })
+ if err != nil {
+ return nil, err
+ }
+ // TODO proper return value?
+ return resp, nil
+}
+
+// SetGroupName updates the name (subject) of the given group on WhatsApp.
+func (cli *Client) SetGroupName(jid types.JID, name string) error {
+ _, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{
+ Tag: "subject",
+ Content: []byte(name),
+ })
+ return err
+}
+
+// SetGroupTopic updates the topic (description) of the given group on WhatsApp.
+//
+// The previousID and newID fields are optional. If the previous ID is not specified, this will
+// automatically fetch the current group info to find the previous topic ID. If the new ID is not
+// specified, one will be generated with GenerateMessageID().
+func (cli *Client) SetGroupTopic(jid types.JID, previousID, newID, topic string) error {
+ if previousID == "" {
+ oldInfo, err := cli.GetGroupInfo(jid)
+ if err != nil {
+ return fmt.Errorf("failed to get old group info to update topic: %v", err)
+ }
+ previousID = oldInfo.TopicID
+ }
+ if newID == "" {
+ newID = GenerateMessageID()
+ }
+ _, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{
+ Tag: "description",
+ Attrs: waBinary.Attrs{
+ "prev": previousID,
+ "id": newID,
+ },
+ Content: []waBinary.Node{{
+ Tag: "body",
+ Content: []byte(topic),
+ }},
+ })
+ return err
+}
+
+// SetGroupLocked changes whether the group is locked (i.e. whether only admins can modify group info).
+func (cli *Client) SetGroupLocked(jid types.JID, locked bool) error {
+ tag := "locked"
+ if !locked {
+ tag = "unlocked"
+ }
+ _, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{Tag: tag})
+ return err
+}
+
+// SetGroupAnnounce changes whether the group is in announce mode (i.e. whether only admins can send messages).
+func (cli *Client) SetGroupAnnounce(jid types.JID, announce bool) error {
+ tag := "announcement"
+ if !announce {
+ tag = "not_announcement"
+ }
+ _, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{Tag: tag})
+ return err
+}
+
+// GetGroupInviteLink requests the invite link to the group from the WhatsApp servers.
+//
+// If reset is true, then the old invite link will be revoked and a new one generated.
+func (cli *Client) GetGroupInviteLink(jid types.JID, reset bool) (string, error) {
+ iqType := iqGet
+ if reset {
+ iqType = iqSet
+ }
+ resp, err := cli.sendGroupIQ(iqType, jid, waBinary.Node{Tag: "invite"})
+ if errors.Is(err, ErrIQNotAuthorized) {
+ return "", wrapIQError(ErrGroupInviteLinkUnauthorized, err)
+ } else if errors.Is(err, ErrIQNotFound) {
+ return "", wrapIQError(ErrGroupNotFound, err)
+ } else if errors.Is(err, ErrIQForbidden) {
+ return "", wrapIQError(ErrNotInGroup, err)
+ } else if err != nil {
+ return "", err
+ }
+ code, ok := resp.GetChildByTag("invite").Attrs["code"].(string)
+ if !ok {
+ return "", fmt.Errorf("didn't find invite code in response")
+ }
+ return InviteLinkPrefix + code, nil
+}
+
+// GetGroupInfoFromInvite gets the group info from an invite message.
+//
+// Note that this is specifically for invite messages, not invite links. Use GetGroupInfoFromLink for resolving chat.whatsapp.com links.
+func (cli *Client) GetGroupInfoFromInvite(jid, inviter types.JID, code string, expiration int64) (*types.GroupInfo, error) {
+ resp, err := cli.sendGroupIQ(iqGet, jid, waBinary.Node{
+ Tag: "query",
+ Content: []waBinary.Node{{
+ Tag: "add_request",
+ Attrs: waBinary.Attrs{
+ "code": code,
+ "expiration": expiration,
+ "admin": inviter,
+ },
+ }},
+ })
+ if err != nil {
+ return nil, err
+ }
+ groupNode, ok := resp.GetOptionalChildByTag("group")
+ if !ok {
+ return nil, &ElementMissingError{Tag: "group", In: "response to invite group info query"}
+ }
+ return cli.parseGroupNode(&groupNode)
+}
+
+// JoinGroupWithInvite joins a group using an invite message.
+//
+// Note that this is specifically for invite messages, not invite links. Use JoinGroupWithLink for joining with chat.whatsapp.com links.
+func (cli *Client) JoinGroupWithInvite(jid, inviter types.JID, code string, expiration int64) error {
+ _, err := cli.sendGroupIQ(iqSet, jid, waBinary.Node{
+ Tag: "accept",
+ Attrs: waBinary.Attrs{
+ "code": code,
+ "expiration": expiration,
+ "admin": inviter,
+ },
+ })
+ return err
+}
+
+// GetGroupInfoFromLink resolves the given invite link and asks the WhatsApp servers for info about the group.
+// This will not cause the user to join the group.
+func (cli *Client) GetGroupInfoFromLink(code string) (*types.GroupInfo, error) {
+ code = strings.TrimPrefix(code, InviteLinkPrefix)
+ resp, err := cli.sendGroupIQ(iqGet, types.GroupServerJID, waBinary.Node{
+ Tag: "invite",
+ Attrs: waBinary.Attrs{"code": code},
+ })
+ if errors.Is(err, ErrIQGone) {
+ return nil, wrapIQError(ErrInviteLinkRevoked, err)
+ } else if errors.Is(err, ErrIQNotAcceptable) {
+ return nil, wrapIQError(ErrInviteLinkInvalid, err)
+ } else if err != nil {
+ return nil, err
+ }
+ groupNode, ok := resp.GetOptionalChildByTag("group")
+ if !ok {
+ return nil, &ElementMissingError{Tag: "group", In: "response to group link info query"}
+ }
+ return cli.parseGroupNode(&groupNode)
+}
+
+// JoinGroupWithLink joins the group using the given invite link.
+func (cli *Client) JoinGroupWithLink(code string) (types.JID, error) {
+ code = strings.TrimPrefix(code, InviteLinkPrefix)
+ resp, err := cli.sendGroupIQ(iqSet, types.GroupServerJID, waBinary.Node{
+ Tag: "invite",
+ Attrs: waBinary.Attrs{"code": code},
+ })
+ if errors.Is(err, ErrIQGone) {
+ return types.EmptyJID, wrapIQError(ErrInviteLinkRevoked, err)
+ } else if errors.Is(err, ErrIQNotAcceptable) {
+ return types.EmptyJID, wrapIQError(ErrInviteLinkInvalid, err)
+ } else if err != nil {
+ return types.EmptyJID, err
+ }
+ groupNode, ok := resp.GetOptionalChildByTag("group")
+ if !ok {
+ return types.EmptyJID, &ElementMissingError{Tag: "group", In: "response to group link join query"}
+ }
+ return groupNode.AttrGetter().JID("jid"), nil
+}
+
+// GetJoinedGroups returns the list of groups the user is participating in.
+func (cli *Client) GetJoinedGroups() ([]*types.GroupInfo, error) {
+ resp, err := cli.sendGroupIQ(iqGet, types.GroupServerJID, waBinary.Node{
+ Tag: "participating",
+ Content: []waBinary.Node{
+ {Tag: "participants"},
+ {Tag: "description"},
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ groups, ok := resp.GetOptionalChildByTag("groups")
+ if !ok {
+ return nil, &ElementMissingError{Tag: "groups", In: "response to group list query"}
+ }
+ children := groups.GetChildren()
+ infos := make([]*types.GroupInfo, 0, len(children))
+ for _, child := range children {
+ if child.Tag != "group" {
+ cli.Log.Debugf("Unexpected child in group list response: %s", child.XMLString())
+ continue
+ }
+ parsed, parseErr := cli.parseGroupNode(&child)
+ if parseErr != nil {
+ cli.Log.Warnf("Error parsing group %s: %v", parsed.JID, parseErr)
+ }
+ infos = append(infos, parsed)
+ }
+ return infos, nil
+}
+
+// GetGroupInfo requests basic info about a group chat from the WhatsApp servers.
+func (cli *Client) GetGroupInfo(jid types.JID) (*types.GroupInfo, error) {
+ return cli.getGroupInfo(jid, true)
+}
+
+func (cli *Client) getGroupInfo(jid types.JID, lockParticipantCache bool) (*types.GroupInfo, error) {
+ res, err := cli.sendGroupIQ(iqGet, jid, waBinary.Node{
+ Tag: "query",
+ Attrs: waBinary.Attrs{"request": "interactive"},
+ })
+ if errors.Is(err, ErrIQNotFound) {
+ return nil, wrapIQError(ErrGroupNotFound, err)
+ } else if errors.Is(err, ErrIQForbidden) {
+ return nil, wrapIQError(ErrNotInGroup, err)
+ } else if err != nil {
+ return nil, err
+ }
+
+ groupNode, ok := res.GetOptionalChildByTag("group")
+ if !ok {
+ return nil, &ElementMissingError{Tag: "groups", In: "response to group info query"}
+ }
+ groupInfo, err := cli.parseGroupNode(&groupNode)
+ if err != nil {
+ return groupInfo, err
+ }
+ if lockParticipantCache {
+ cli.groupParticipantsCacheLock.Lock()
+ defer cli.groupParticipantsCacheLock.Unlock()
+ }
+ participants := make([]types.JID, len(groupInfo.Participants))
+ for i, part := range groupInfo.Participants {
+ participants[i] = part.JID
+ }
+ cli.groupParticipantsCache[jid] = participants
+ return groupInfo, nil
+}
+
+func (cli *Client) getGroupMembers(jid types.JID) ([]types.JID, error) {
+ cli.groupParticipantsCacheLock.Lock()
+ defer cli.groupParticipantsCacheLock.Unlock()
+ if _, ok := cli.groupParticipantsCache[jid]; !ok {
+ _, err := cli.getGroupInfo(jid, false)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return cli.groupParticipantsCache[jid], nil
+}
+
+func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, error) {
+ var group types.GroupInfo
+ ag := groupNode.AttrGetter()
+
+ group.JID = types.NewJID(ag.String("id"), types.GroupServer)
+ group.OwnerJID = ag.OptionalJIDOrEmpty("creator")
+
+ group.Name = ag.String("subject")
+ group.NameSetAt = time.Unix(ag.Int64("s_t"), 0)
+ group.NameSetBy = ag.OptionalJIDOrEmpty("s_o")
+
+ group.GroupCreated = time.Unix(ag.Int64("creation"), 0)
+
+ group.AnnounceVersionID = ag.OptionalString("a_v_id")
+ group.ParticipantVersionID = ag.OptionalString("p_v_id")
+
+ for _, child := range groupNode.GetChildren() {
+ childAG := child.AttrGetter()
+ switch child.Tag {
+ case "participant":
+ pcpType := childAG.OptionalString("type")
+ participant := types.GroupParticipant{
+ IsAdmin: pcpType == "admin" || pcpType == "superadmin",
+ IsSuperAdmin: pcpType == "superadmin",
+ JID: childAG.JID("jid"),
+ }
+ group.Participants = append(group.Participants, participant)
+ case "description":
+ body, bodyOK := child.GetOptionalChildByTag("body")
+ if bodyOK {
+ group.Topic, _ = body.Content.(string)
+ group.TopicID = childAG.String("id")
+ group.TopicSetBy = childAG.OptionalJIDOrEmpty("participant")
+ group.TopicSetAt = time.Unix(childAG.Int64("t"), 0)
+ }
+ case "announcement":
+ group.IsAnnounce = true
+ case "locked":
+ group.IsLocked = true
+ case "ephemeral":
+ group.IsEphemeral = true
+ group.DisappearingTimer = uint32(childAG.Uint64("expiration"))
+ default:
+ cli.Log.Debugf("Unknown element in group node %s: %s", group.JID.String(), child.XMLString())
+ }
+ if !childAG.OK() {
+ cli.Log.Warnf("Possibly failed to parse %s element in group node: %+v", child.Tag, childAG.Errors)
+ }
+ }
+
+ return &group, ag.Error()
+}
+
+func parseParticipantList(node *waBinary.Node) (participants []types.JID) {
+ children := node.GetChildren()
+ participants = make([]types.JID, 0, len(children))
+ for _, child := range children {
+ jid, ok := child.Attrs["jid"].(types.JID)
+ if child.Tag != "participant" || !ok {
+ continue
+ }
+ participants = append(participants, jid)
+ }
+ return
+}
+
+func (cli *Client) parseGroupCreate(node *waBinary.Node) (*events.JoinedGroup, error) {
+ groupNode, ok := node.GetOptionalChildByTag("group")
+ if !ok {
+ return nil, fmt.Errorf("group create notification didn't contain group info")
+ }
+ var evt events.JoinedGroup
+ evt.Reason = node.AttrGetter().OptionalString("reason")
+ info, err := cli.parseGroupNode(&groupNode)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse group info in create notification: %w", err)
+ }
+ evt.GroupInfo = *info
+ return &evt, nil
+}
+
+func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, error) {
+ var evt events.GroupInfo
+ ag := node.AttrGetter()
+ evt.JID = ag.JID("from")
+ evt.Notify = ag.OptionalString("notify")
+ evt.Sender = ag.OptionalJID("participant")
+ evt.Timestamp = time.Unix(ag.Int64("t"), 0)
+ if !ag.OK() {
+ return nil, fmt.Errorf("group change doesn't contain required attributes: %w", ag.Error())
+ }
+
+ for _, child := range node.GetChildren() {
+ cag := child.AttrGetter()
+ if child.Tag == "add" || child.Tag == "remove" || child.Tag == "promote" || child.Tag == "demote" {
+ evt.PrevParticipantVersionID = cag.String("prev_v_id")
+ evt.ParticipantVersionID = cag.String("v_id")
+ }
+ switch child.Tag {
+ case "add":
+ evt.JoinReason = cag.OptionalString("reason")
+ evt.Join = parseParticipantList(&child)
+ case "remove":
+ evt.Leave = parseParticipantList(&child)
+ case "promote":
+ evt.Promote = parseParticipantList(&child)
+ case "demote":
+ evt.Demote = parseParticipantList(&child)
+ case "locked":
+ evt.Locked = &types.GroupLocked{IsLocked: true}
+ case "unlocked":
+ evt.Locked = &types.GroupLocked{IsLocked: false}
+ case "subject":
+ evt.Name = &types.GroupName{
+ Name: cag.String("subject"),
+ NameSetAt: time.Unix(cag.Int64("s_t"), 0),
+ NameSetBy: cag.OptionalJIDOrEmpty("s_o"),
+ }
+ case "description":
+ topicChild := child.GetChildByTag("body")
+ topicBytes, ok := topicChild.Content.([]byte)
+ if !ok {
+ return nil, fmt.Errorf("group change description has unexpected body: %s", topicChild.XMLString())
+ }
+ var setBy types.JID
+ if evt.Sender != nil {
+ setBy = *evt.Sender
+ }
+ evt.Topic = &types.GroupTopic{
+ Topic: string(topicBytes),
+ TopicID: cag.String("id"),
+ TopicSetAt: evt.Timestamp,
+ TopicSetBy: setBy,
+ }
+ case "announcement":
+ evt.Announce = &types.GroupAnnounce{
+ IsAnnounce: true,
+ AnnounceVersionID: cag.String("v_id"),
+ }
+ case "not_announcement":
+ evt.Announce = &types.GroupAnnounce{
+ IsAnnounce: false,
+ AnnounceVersionID: cag.String("v_id"),
+ }
+ case "invite":
+ link := InviteLinkPrefix + cag.String("code")
+ evt.NewInviteLink = &link
+ case "ephemeral":
+ timer := uint32(cag.Uint64("expiration"))
+ evt.Ephemeral = &types.GroupEphemeral{
+ IsEphemeral: true,
+ DisappearingTimer: timer,
+ }
+ case "not_ephemeral":
+ evt.Ephemeral = &types.GroupEphemeral{IsEphemeral: false}
+ default:
+ evt.UnknownChanges = append(evt.UnknownChanges, &child)
+ }
+ if !cag.OK() {
+ return nil, fmt.Errorf("group change %s element doesn't contain required attributes: %w", child.Tag, cag.Error())
+ }
+ }
+ return &evt, nil
+}
+
+func (cli *Client) updateGroupParticipantCache(evt *events.GroupInfo) {
+ if len(evt.Join) == 0 && len(evt.Leave) == 0 {
+ return
+ }
+ cli.groupParticipantsCacheLock.Lock()
+ defer cli.groupParticipantsCacheLock.Unlock()
+ cached, ok := cli.groupParticipantsCache[evt.JID]
+ if !ok {
+ return
+ }
+Outer:
+ for _, jid := range evt.Join {
+ for _, existingJID := range cached {
+ if jid == existingJID {
+ continue Outer
+ }
+ }
+ cached = append(cached, jid)
+ }
+ for _, jid := range evt.Leave {
+ for i, existingJID := range cached {
+ if existingJID == jid {
+ cached[i] = cached[len(cached)-1]
+ cached = cached[:len(cached)-1]
+ break
+ }
+ }
+ }
+ cli.groupParticipantsCache[evt.JID] = cached
+}
+
+func (cli *Client) parseGroupNotification(node *waBinary.Node) (interface{}, error) {
+ children := node.GetChildren()
+ if len(children) == 1 && children[0].Tag == "create" {
+ return cli.parseGroupCreate(&children[0])
+ } else {
+ groupChange, err := cli.parseGroupChange(node)
+ if err != nil {
+ return nil, err
+ }
+ cli.updateGroupParticipantCache(groupChange)
+ return groupChange, nil
+ }
+}