From 6566a0534020979877a49b1a5caa8cdc97c9c55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=89=BE=E5=AE=89=E9=8A=B3?= Date: Wed, 6 Dec 2023 19:26:00 +0700 Subject: [PATCH] Fix making multiple changes at a time with `UpdateGroupParticipants` (#495) Co-authored-by: Tulir Asokan --- group.go | 147 +++++++++++++++++++++++++++++++++++-------------- mdtest/main.go | 64 +++++++++++++++++++++ 2 files changed, 171 insertions(+), 40 deletions(-) diff --git a/group.go b/group.go index 94a82f6e..199493c8 100644 --- a/group.go +++ b/group.go @@ -148,30 +148,93 @@ const ( ) // 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) { +func (cli *Client) UpdateGroupParticipants(jid types.JID, participantChanges []types.JID, action ParticipantChange) ([]types.GroupParticipant, error) { content := make([]waBinary.Node, len(participantChanges)) - i := 0 - for participantJID, change := range participantChanges { + for i, participantJID := range participantChanges { content[i] = waBinary.Node{ - Tag: string(change), - Content: []waBinary.Node{{ - Tag: "participant", - Attrs: waBinary.Attrs{"jid": participantJID}, - }}, + Tag: "participant", + Attrs: waBinary.Attrs{"jid": participantJID}, } - i++ } - resp, err := cli.sendIQ(infoQuery{ - Namespace: "w:g2", - Type: iqSet, - To: jid, - Content: content, + resp, err := cli.sendGroupIQ(context.TODO(), iqSet, jid, waBinary.Node{ + Tag: string(action), + Content: content, }) if err != nil { return nil, err } - // TODO proper return value? - return resp, nil + requestAction, ok := resp.GetOptionalChildByTag(string(action)) + if !ok { + return nil, &ElementMissingError{Tag: string(action), In: "response to group participants update"} + } + requestParticipants := requestAction.GetChildrenByTag("participant") + participants := make([]types.GroupParticipant, len(requestParticipants)) + for i, child := range requestParticipants { + participants[i] = parseParticipant(child.AttrGetter(), &child) + } + return participants, nil +} + +// GetGroupRequestParticipants gets the list of participants that have requested to join the group. +func (cli *Client) GetGroupRequestParticipants(jid types.JID) ([]types.JID, error) { + resp, err := cli.sendGroupIQ(context.TODO(), iqGet, jid, waBinary.Node{ + Tag: "membership_approval_requests", + }) + if err != nil { + return nil, err + } + request, ok := resp.GetOptionalChildByTag("membership_approval_requests") + if !ok { + return nil, &ElementMissingError{Tag: "membership_approval_requests", In: "response to group request participants query"} + } + requestParticipants := request.GetChildrenByTag("membership_approval_request") + participants := make([]types.JID, len(requestParticipants)) + for i, req := range requestParticipants { + participants[i] = req.AttrGetter().JID("jid") + } + return participants, nil +} + +type ParticipantRequestChange string + +const ( + ParticipantChangeApprove ParticipantRequestChange = "approve" + ParticipantChangeReject ParticipantRequestChange = "reject" +) + +// UpdateGroupRequestParticipants can be used to approve or reject requests to join the group. +func (cli *Client) UpdateGroupRequestParticipants(jid types.JID, participantChanges []types.JID, action ParticipantRequestChange) ([]types.GroupParticipant, error) { + content := make([]waBinary.Node, len(participantChanges)) + for i, participantJID := range participantChanges { + content[i] = waBinary.Node{ + Tag: "participant", + Attrs: waBinary.Attrs{"jid": participantJID}, + } + } + resp, err := cli.sendGroupIQ(context.TODO(), iqSet, jid, waBinary.Node{ + Tag: "membership_requests_action", + Content: []waBinary.Node{{ + Tag: string(action), + Content: content, + }}, + }) + if err != nil { + return nil, err + } + request, ok := resp.GetOptionalChildByTag("membership_requests_action") + if !ok { + return nil, &ElementMissingError{Tag: "membership_requests_action", In: "response to group request participants update"} + } + requestAction, ok := request.GetOptionalChildByTag(string(action)) + if !ok { + return nil, &ElementMissingError{Tag: string(action), In: "response to group request participants update"} + } + requestParticipants := requestAction.GetChildrenByTag("participant") + participants := make([]types.GroupParticipant, len(requestParticipants)) + for i, child := range requestParticipants { + participants[i] = parseParticipant(child.AttrGetter(), &child) + } + return participants, nil } // SetGroupPhoto updates the group picture/icon of the given group on WhatsApp. @@ -501,6 +564,33 @@ func (cli *Client) getGroupMembers(ctx context.Context, jid types.JID) ([]types. return cli.groupParticipantsCache[jid], nil } +func parseParticipant(childAG *waBinary.AttrUtility, child *waBinary.Node) types.GroupParticipant { + pcpType := childAG.OptionalString("type") + participant := types.GroupParticipant{ + IsAdmin: pcpType == "admin" || pcpType == "superadmin", + IsSuperAdmin: pcpType == "superadmin", + JID: childAG.JID("jid"), + LID: childAG.OptionalJIDOrEmpty("lid"), + DisplayName: childAG.OptionalString("display_name"), + } + if participant.JID.Server == types.HiddenUserServer && participant.LID.IsEmpty() { + participant.LID = participant.JID + //participant.JID = types.EmptyJID + } + if errorCode := childAG.OptionalInt("error"); errorCode != 0 { + participant.Error = errorCode + addRequest, ok := child.GetOptionalChildByTag("add_request") + if ok { + addAG := addRequest.AttrGetter() + participant.AddRequest = &types.GroupParticipantAddRequest{ + Code: addAG.String("code"), + Expiration: addAG.UnixTime("expiration"), + } + } + } + return participant +} + func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, error) { var group types.GroupInfo ag := groupNode.AttrGetter() @@ -521,30 +611,7 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e 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"), - LID: childAG.OptionalJIDOrEmpty("lid"), - DisplayName: childAG.OptionalString("display_name"), - } - if participant.JID.Server == types.HiddenUserServer && participant.LID.IsEmpty() { - participant.LID = participant.JID - //participant.JID = types.EmptyJID - } - if errorCode := childAG.OptionalInt("error"); errorCode != 0 { - participant.Error = errorCode - addRequest, ok := child.GetOptionalChildByTag("add_request") - if ok { - addAG := addRequest.AttrGetter() - participant.AddRequest = &types.GroupParticipantAddRequest{ - Code: addAG.String("code"), - Expiration: addAG.UnixTime("expiration"), - } - } - } - group.Participants = append(group.Participants, participant) + group.Participants = append(group.Participants, parseParticipant(childAG, &child)) case "description": body, bodyOK := child.GetOptionalChildByTag("body") if bodyOK { diff --git a/mdtest/main.go b/mdtest/main.go index 3f90610e..4a88139a 100644 --- a/mdtest/main.go +++ b/mdtest/main.go @@ -607,6 +607,70 @@ func handleCmd(cmd string, args []string) { } else { log.Infof("Joined %s", groupID) } + case "updateparticipant": + if len(args) < 3 { + log.Errorf("Usage: updateparticipant ") + return + } + jid, ok := parseJID(args[0]) + if !ok { + return + } + action := whatsmeow.ParticipantChange(args[1]) + switch action { + case whatsmeow.ParticipantChangeAdd, whatsmeow.ParticipantChangeRemove, whatsmeow.ParticipantChangePromote, whatsmeow.ParticipantChangeDemote: + default: + log.Errorf("Valid actions: add, remove, promote, demote") + return + } + users := make([]types.JID, len(args)-2) + for i, arg := range args[2:] { + users[i], ok = parseJID(arg) + if !ok { + return + } + } + resp, err := cli.UpdateGroupParticipants(jid, users, action) + if err != nil { + log.Errorf("Failed to add participant: %v", err) + return + } + for _, item := range resp { + if action == whatsmeow.ParticipantChangeAdd && item.Error == 403 && item.AddRequest != nil { + log.Infof("Participant is private: %d %s %s %v", item.Error, item.JID, item.AddRequest.Code, item.AddRequest.Expiration) + cli.SendMessage(context.TODO(), item.JID, &waProto.Message{ + GroupInviteMessage: &waProto.GroupInviteMessage{ + InviteCode: proto.String(item.AddRequest.Code), + InviteExpiration: proto.Int64(item.AddRequest.Expiration.Unix()), + GroupJid: proto.String(jid.String()), + GroupName: proto.String("Test group"), + Caption: proto.String("This is a test group"), + }, + }) + } else if item.Error == 409 { + log.Infof("Participant already in group: %d %s %+v", item.Error, item.JID) + } else if item.Error == 0 { + log.Infof("Added participant: %d %s %+v", item.Error, item.JID) + } else { + log.Infof("Unknown status: %d %s %+v", item.Error, item.JID) + } + } + case "getrequestparticipant": + if len(args) < 1 { + log.Errorf("Usage: getrequestparticipant ") + return + } + group, ok := parseJID(args[0]) + if !ok { + log.Errorf("Invalid JID") + return + } + resp, err := cli.GetGroupRequestParticipants(group) + if err != nil { + log.Errorf("Failed to get request participants: %v", err) + } else { + log.Infof("Request participants: %+v", resp) + } case "getstatusprivacy": resp, err := cli.GetStatusPrivacy() fmt.Println(err)