Skip to content

Commit

Permalink
Fix making multiple changes at a time with UpdateGroupParticipants (#…
Browse files Browse the repository at this point in the history
…495)

Co-authored-by: Tulir Asokan <tulir@maunium.net>
  • Loading branch information
arugaz and tulir authored Dec 6, 2023
1 parent 9ddc2f3 commit 6566a05
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 40 deletions.
147 changes: 107 additions & 40 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand Down
64 changes: 64 additions & 0 deletions mdtest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <jid> <action> <numbers...>")
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 <jid>")
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)
Expand Down

0 comments on commit 6566a05

Please sign in to comment.