diff options
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/group.go')
-rw-r--r-- | vendor/go.mau.fi/whatsmeow/group.go | 566 |
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 + } +} |