group.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067
  1. // Copyright (c) 2021 Tulir Asokan
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. package whatsmeow
  7. import (
  8. "context"
  9. "errors"
  10. "fmt"
  11. "strings"
  12. waBinary "go.mau.fi/whatsmeow/binary"
  13. "go.mau.fi/whatsmeow/store"
  14. "go.mau.fi/whatsmeow/types"
  15. "go.mau.fi/whatsmeow/types/events"
  16. )
  17. const InviteLinkPrefix = "https://chat.whatsapp.com/"
  18. func (cli *Client) sendGroupIQ(ctx context.Context, iqType infoQueryType, jid types.JID, content waBinary.Node) (*waBinary.Node, error) {
  19. return cli.sendIQ(ctx, infoQuery{
  20. Namespace: "w:g2",
  21. Type: iqType,
  22. To: jid,
  23. Content: []waBinary.Node{content},
  24. })
  25. }
  26. // ReqCreateGroup contains the request data for CreateGroup.
  27. type ReqCreateGroup struct {
  28. // Group names are limited to 25 characters. A longer group name will cause a 406 not acceptable error.
  29. Name string
  30. // You don't need to include your own JID in the participants array, the WhatsApp servers will add it implicitly.
  31. Participants []types.JID
  32. // A create key can be provided to deduplicate the group create notification that will be triggered
  33. // when the group is created. If provided, the JoinedGroup event will contain the same key.
  34. CreateKey types.MessageID
  35. types.GroupEphemeral
  36. types.GroupAnnounce
  37. types.GroupLocked
  38. types.GroupMembershipApprovalMode
  39. // Set IsParent to true to create a community instead of a normal group.
  40. // When creating a community, the linked announcement group will be created automatically by the server.
  41. types.GroupParent
  42. // Set LinkedParentJID to create a group inside a community.
  43. types.GroupLinkedParent
  44. }
  45. // CreateGroup creates a group on WhatsApp with the given name and participants.
  46. //
  47. // See ReqCreateGroup for parameters.
  48. func (cli *Client) CreateGroup(ctx context.Context, req ReqCreateGroup) (*types.GroupInfo, error) {
  49. participantNodes := make([]waBinary.Node, len(req.Participants), len(req.Participants)+1)
  50. for i, participant := range req.Participants {
  51. participantNodes[i] = waBinary.Node{
  52. Tag: "participant",
  53. Attrs: waBinary.Attrs{"jid": participant},
  54. }
  55. pt, err := cli.Store.PrivacyTokens.GetPrivacyToken(ctx, participant)
  56. if err != nil {
  57. return nil, fmt.Errorf("failed to get privacy token for participant %s: %v", participant, err)
  58. } else if pt != nil {
  59. participantNodes[i].Content = []waBinary.Node{{
  60. Tag: "privacy",
  61. Content: pt.Token,
  62. }}
  63. }
  64. }
  65. if req.CreateKey == "" {
  66. req.CreateKey = cli.GenerateMessageID()
  67. }
  68. if req.IsParent {
  69. if req.DefaultMembershipApprovalMode == "" {
  70. req.DefaultMembershipApprovalMode = "request_required"
  71. }
  72. participantNodes = append(participantNodes, waBinary.Node{
  73. Tag: "parent",
  74. Attrs: waBinary.Attrs{
  75. "default_membership_approval_mode": req.DefaultMembershipApprovalMode,
  76. },
  77. })
  78. } else if !req.LinkedParentJID.IsEmpty() {
  79. participantNodes = append(participantNodes, waBinary.Node{
  80. Tag: "linked_parent",
  81. Attrs: waBinary.Attrs{"jid": req.LinkedParentJID},
  82. })
  83. }
  84. if req.IsLocked {
  85. participantNodes = append(participantNodes, waBinary.Node{Tag: "locked"})
  86. }
  87. if req.IsAnnounce {
  88. participantNodes = append(participantNodes, waBinary.Node{Tag: "announcement"})
  89. }
  90. if req.IsEphemeral {
  91. participantNodes = append(participantNodes, waBinary.Node{
  92. Tag: "ephemeral",
  93. Attrs: waBinary.Attrs{
  94. "expiration": req.DisappearingTimer,
  95. "trigger": "1", // TODO what's this?
  96. },
  97. })
  98. }
  99. if req.IsJoinApprovalRequired {
  100. participantNodes = append(participantNodes, waBinary.Node{
  101. Tag: "membership_approval_mode",
  102. Content: []waBinary.Node{{
  103. Tag: "group_join",
  104. Attrs: waBinary.Attrs{"state": "on"},
  105. }},
  106. })
  107. }
  108. // WhatsApp web doesn't seem to include the static prefix for these
  109. key := strings.TrimPrefix(req.CreateKey, "3EB0")
  110. resp, err := cli.sendGroupIQ(ctx, iqSet, types.GroupServerJID, waBinary.Node{
  111. Tag: "create",
  112. Attrs: waBinary.Attrs{
  113. "subject": req.Name,
  114. "key": key,
  115. },
  116. Content: participantNodes,
  117. })
  118. if err != nil {
  119. return nil, err
  120. }
  121. groupNode, ok := resp.GetOptionalChildByTag("group")
  122. if !ok {
  123. return nil, &ElementMissingError{Tag: "group", In: "response to create group query"}
  124. }
  125. return cli.parseGroupNode(&groupNode)
  126. }
  127. // UnlinkGroup removes a child group from a parent community.
  128. func (cli *Client) UnlinkGroup(ctx context.Context, parent, child types.JID) error {
  129. _, err := cli.sendGroupIQ(ctx, iqSet, parent, waBinary.Node{
  130. Tag: "unlink",
  131. Attrs: waBinary.Attrs{"unlink_type": string(types.GroupLinkChangeTypeSub)},
  132. Content: []waBinary.Node{{
  133. Tag: "group",
  134. Attrs: waBinary.Attrs{"jid": child},
  135. }},
  136. })
  137. return err
  138. }
  139. // LinkGroup adds an existing group as a child group in a community.
  140. //
  141. // To create a new group within a community, set LinkedParentJID in the CreateGroup request.
  142. func (cli *Client) LinkGroup(ctx context.Context, parent, child types.JID) error {
  143. _, err := cli.sendGroupIQ(ctx, iqSet, parent, waBinary.Node{
  144. Tag: "links",
  145. Content: []waBinary.Node{{
  146. Tag: "link",
  147. Attrs: waBinary.Attrs{"link_type": string(types.GroupLinkChangeTypeSub)},
  148. Content: []waBinary.Node{{
  149. Tag: "group",
  150. Attrs: waBinary.Attrs{"jid": child},
  151. }},
  152. }},
  153. })
  154. return err
  155. }
  156. // LeaveGroup leaves the specified group on WhatsApp.
  157. func (cli *Client) LeaveGroup(ctx context.Context, jid types.JID) error {
  158. _, err := cli.sendGroupIQ(ctx, iqSet, types.GroupServerJID, waBinary.Node{
  159. Tag: "leave",
  160. Content: []waBinary.Node{{
  161. Tag: "group",
  162. Attrs: waBinary.Attrs{"id": jid},
  163. }},
  164. })
  165. return err
  166. }
  167. type ParticipantChange string
  168. const (
  169. ParticipantChangeAdd ParticipantChange = "add"
  170. ParticipantChangeRemove ParticipantChange = "remove"
  171. ParticipantChangePromote ParticipantChange = "promote"
  172. ParticipantChangeDemote ParticipantChange = "demote"
  173. )
  174. // UpdateGroupParticipants can be used to add, remove, promote and demote members in a WhatsApp group.
  175. func (cli *Client) UpdateGroupParticipants(ctx context.Context, jid types.JID, participantChanges []types.JID, action ParticipantChange) ([]types.GroupParticipant, error) {
  176. content := make([]waBinary.Node, len(participantChanges))
  177. for i, participantJID := range participantChanges {
  178. content[i] = waBinary.Node{
  179. Tag: "participant",
  180. Attrs: waBinary.Attrs{"jid": participantJID},
  181. }
  182. if participantJID.Server == types.HiddenUserServer && action == ParticipantChangeAdd {
  183. pn, err := cli.Store.LIDs.GetPNForLID(ctx, participantJID)
  184. if err != nil {
  185. return nil, fmt.Errorf("failed to get phone number for LID %s: %v", participantJID, err)
  186. } else if !pn.IsEmpty() {
  187. content[i].Attrs["phone_number"] = pn
  188. }
  189. }
  190. }
  191. resp, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{
  192. Tag: string(action),
  193. Content: content,
  194. })
  195. if err != nil {
  196. return nil, err
  197. }
  198. requestAction, ok := resp.GetOptionalChildByTag(string(action))
  199. if !ok {
  200. return nil, &ElementMissingError{Tag: string(action), In: "response to group participants update"}
  201. }
  202. requestParticipants := requestAction.GetChildrenByTag("participant")
  203. participants := make([]types.GroupParticipant, len(requestParticipants))
  204. for i, child := range requestParticipants {
  205. participants[i] = parseParticipant(child.AttrGetter(), &child)
  206. }
  207. return participants, nil
  208. }
  209. // GetGroupRequestParticipants gets the list of participants that have requested to join the group.
  210. func (cli *Client) GetGroupRequestParticipants(ctx context.Context, jid types.JID) ([]types.GroupParticipantRequest, error) {
  211. resp, err := cli.sendGroupIQ(ctx, iqGet, jid, waBinary.Node{
  212. Tag: "membership_approval_requests",
  213. })
  214. if err != nil {
  215. return nil, err
  216. }
  217. request, ok := resp.GetOptionalChildByTag("membership_approval_requests")
  218. if !ok {
  219. return nil, &ElementMissingError{Tag: "membership_approval_requests", In: "response to group request participants query"}
  220. }
  221. requestParticipants := request.GetChildrenByTag("membership_approval_request")
  222. participants := make([]types.GroupParticipantRequest, len(requestParticipants))
  223. for i, req := range requestParticipants {
  224. participants[i] = types.GroupParticipantRequest{
  225. JID: req.AttrGetter().JID("jid"),
  226. RequestedAt: req.AttrGetter().UnixTime("request_time"),
  227. }
  228. }
  229. return participants, nil
  230. }
  231. type ParticipantRequestChange string
  232. const (
  233. ParticipantChangeApprove ParticipantRequestChange = "approve"
  234. ParticipantChangeReject ParticipantRequestChange = "reject"
  235. )
  236. // UpdateGroupRequestParticipants can be used to approve or reject requests to join the group.
  237. func (cli *Client) UpdateGroupRequestParticipants(ctx context.Context, jid types.JID, participantChanges []types.JID, action ParticipantRequestChange) ([]types.GroupParticipant, error) {
  238. content := make([]waBinary.Node, len(participantChanges))
  239. for i, participantJID := range participantChanges {
  240. content[i] = waBinary.Node{
  241. Tag: "participant",
  242. Attrs: waBinary.Attrs{"jid": participantJID},
  243. }
  244. }
  245. resp, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{
  246. Tag: "membership_requests_action",
  247. Content: []waBinary.Node{{
  248. Tag: string(action),
  249. Content: content,
  250. }},
  251. })
  252. if err != nil {
  253. return nil, err
  254. }
  255. request, ok := resp.GetOptionalChildByTag("membership_requests_action")
  256. if !ok {
  257. return nil, &ElementMissingError{Tag: "membership_requests_action", In: "response to group request participants update"}
  258. }
  259. requestAction, ok := request.GetOptionalChildByTag(string(action))
  260. if !ok {
  261. return nil, &ElementMissingError{Tag: string(action), In: "response to group request participants update"}
  262. }
  263. requestParticipants := requestAction.GetChildrenByTag("participant")
  264. participants := make([]types.GroupParticipant, len(requestParticipants))
  265. for i, child := range requestParticipants {
  266. participants[i] = parseParticipant(child.AttrGetter(), &child)
  267. }
  268. return participants, nil
  269. }
  270. // SetGroupPhoto updates the group picture/icon of the given group on WhatsApp.
  271. // The avatar should be a JPEG photo, other formats may be rejected with ErrInvalidImageFormat.
  272. // The bytes can be nil to remove the photo. Returns the new picture ID.
  273. func (cli *Client) SetGroupPhoto(ctx context.Context, jid types.JID, avatar []byte) (string, error) {
  274. var content interface{}
  275. if avatar != nil {
  276. content = []waBinary.Node{{
  277. Tag: "picture",
  278. Attrs: waBinary.Attrs{"type": "image"},
  279. Content: avatar,
  280. }}
  281. }
  282. resp, err := cli.sendIQ(ctx, infoQuery{
  283. Namespace: "w:profile:picture",
  284. Type: iqSet,
  285. To: types.ServerJID,
  286. Target: jid,
  287. Content: content,
  288. })
  289. if errors.Is(err, ErrIQNotAcceptable) {
  290. return "", wrapIQError(ErrInvalidImageFormat, err)
  291. } else if err != nil {
  292. return "", err
  293. }
  294. if avatar == nil {
  295. return "remove", nil
  296. }
  297. pictureID, ok := resp.GetChildByTag("picture").Attrs["id"].(string)
  298. if !ok {
  299. return "", fmt.Errorf("didn't find picture ID in response")
  300. }
  301. return pictureID, nil
  302. }
  303. // SetGroupName updates the name (subject) of the given group on WhatsApp.
  304. func (cli *Client) SetGroupName(ctx context.Context, jid types.JID, name string) error {
  305. _, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{
  306. Tag: "subject",
  307. Content: []byte(name),
  308. })
  309. return err
  310. }
  311. // SetGroupTopic updates the topic (description) of the given group on WhatsApp.
  312. //
  313. // The previousID and newID fields are optional. If the previous ID is not specified, this will
  314. // automatically fetch the current group info to find the previous topic ID. If the new ID is not
  315. // specified, one will be generated with Client.GenerateMessageID().
  316. func (cli *Client) SetGroupTopic(ctx context.Context, jid types.JID, previousID, newID, topic string) error {
  317. if previousID == "" {
  318. oldInfo, err := cli.GetGroupInfo(ctx, jid)
  319. if err != nil {
  320. return fmt.Errorf("failed to get old group info to update topic: %v", err)
  321. }
  322. previousID = oldInfo.TopicID
  323. }
  324. if newID == "" {
  325. newID = cli.GenerateMessageID()
  326. }
  327. attrs := waBinary.Attrs{
  328. "id": newID,
  329. }
  330. if previousID != "" {
  331. attrs["prev"] = previousID
  332. }
  333. content := []waBinary.Node{{
  334. Tag: "body",
  335. Content: []byte(topic),
  336. }}
  337. if len(topic) == 0 {
  338. attrs["delete"] = "true"
  339. content = nil
  340. }
  341. _, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{
  342. Tag: "description",
  343. Attrs: attrs,
  344. Content: content,
  345. })
  346. return err
  347. }
  348. // SetGroupLocked changes whether the group is locked (i.e. whether only admins can modify group info).
  349. func (cli *Client) SetGroupLocked(ctx context.Context, jid types.JID, locked bool) error {
  350. tag := "locked"
  351. if !locked {
  352. tag = "unlocked"
  353. }
  354. _, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{Tag: tag})
  355. return err
  356. }
  357. // SetGroupAnnounce changes whether the group is in announce mode (i.e. whether only admins can send messages).
  358. func (cli *Client) SetGroupAnnounce(ctx context.Context, jid types.JID, announce bool) error {
  359. tag := "announcement"
  360. if !announce {
  361. tag = "not_announcement"
  362. }
  363. _, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{Tag: tag})
  364. return err
  365. }
  366. // GetGroupInviteLink requests the invite link to the group from the WhatsApp servers.
  367. //
  368. // If reset is true, then the old invite link will be revoked and a new one generated.
  369. func (cli *Client) GetGroupInviteLink(ctx context.Context, jid types.JID, reset bool) (string, error) {
  370. iqType := iqGet
  371. if reset {
  372. iqType = iqSet
  373. }
  374. resp, err := cli.sendGroupIQ(ctx, iqType, jid, waBinary.Node{Tag: "invite"})
  375. if errors.Is(err, ErrIQNotAuthorized) {
  376. return "", wrapIQError(ErrGroupInviteLinkUnauthorized, err)
  377. } else if errors.Is(err, ErrIQNotFound) {
  378. return "", wrapIQError(ErrGroupNotFound, err)
  379. } else if errors.Is(err, ErrIQForbidden) {
  380. return "", wrapIQError(ErrNotInGroup, err)
  381. } else if err != nil {
  382. return "", err
  383. }
  384. code, ok := resp.GetChildByTag("invite").Attrs["code"].(string)
  385. if !ok {
  386. return "", fmt.Errorf("didn't find invite code in response")
  387. }
  388. return InviteLinkPrefix + code, nil
  389. }
  390. // GetGroupInfoFromInvite gets the group info from an invite message.
  391. //
  392. // Note that this is specifically for invite messages, not invite links. Use GetGroupInfoFromLink for resolving chat.whatsapp.com links.
  393. func (cli *Client) GetGroupInfoFromInvite(ctx context.Context, jid, inviter types.JID, code string, expiration int64) (*types.GroupInfo, error) {
  394. resp, err := cli.sendGroupIQ(ctx, iqGet, jid, waBinary.Node{
  395. Tag: "query",
  396. Content: []waBinary.Node{{
  397. Tag: "add_request",
  398. Attrs: waBinary.Attrs{
  399. "code": code,
  400. "expiration": expiration,
  401. "admin": inviter,
  402. },
  403. }},
  404. })
  405. if err != nil {
  406. return nil, err
  407. }
  408. groupNode, ok := resp.GetOptionalChildByTag("group")
  409. if !ok {
  410. return nil, &ElementMissingError{Tag: "group", In: "response to invite group info query"}
  411. }
  412. return cli.parseGroupNode(&groupNode)
  413. }
  414. // JoinGroupWithInvite joins a group using an invite message.
  415. //
  416. // Note that this is specifically for invite messages, not invite links. Use JoinGroupWithLink for joining with chat.whatsapp.com links.
  417. func (cli *Client) JoinGroupWithInvite(ctx context.Context, jid, inviter types.JID, code string, expiration int64) error {
  418. _, err := cli.sendGroupIQ(ctx, iqSet, jid, waBinary.Node{
  419. Tag: "accept",
  420. Attrs: waBinary.Attrs{
  421. "code": code,
  422. "expiration": expiration,
  423. "admin": inviter,
  424. },
  425. })
  426. return err
  427. }
  428. // GetGroupInfoFromLink resolves the given invite link and asks the WhatsApp servers for info about the group.
  429. // This will not cause the user to join the group.
  430. func (cli *Client) GetGroupInfoFromLink(ctx context.Context, code string) (*types.GroupInfo, error) {
  431. code = strings.TrimPrefix(code, InviteLinkPrefix)
  432. resp, err := cli.sendGroupIQ(ctx, iqGet, types.GroupServerJID, waBinary.Node{
  433. Tag: "invite",
  434. Attrs: waBinary.Attrs{"code": code},
  435. })
  436. if errors.Is(err, ErrIQGone) {
  437. return nil, wrapIQError(ErrInviteLinkRevoked, err)
  438. } else if errors.Is(err, ErrIQNotAcceptable) {
  439. return nil, wrapIQError(ErrInviteLinkInvalid, err)
  440. } else if err != nil {
  441. return nil, err
  442. }
  443. groupNode, ok := resp.GetOptionalChildByTag("group")
  444. if !ok {
  445. return nil, &ElementMissingError{Tag: "group", In: "response to group link info query"}
  446. }
  447. return cli.parseGroupNode(&groupNode)
  448. }
  449. // JoinGroupWithLink joins the group using the given invite link.
  450. func (cli *Client) JoinGroupWithLink(ctx context.Context, code string) (types.JID, error) {
  451. code = strings.TrimPrefix(code, InviteLinkPrefix)
  452. resp, err := cli.sendGroupIQ(ctx, iqSet, types.GroupServerJID, waBinary.Node{
  453. Tag: "invite",
  454. Attrs: waBinary.Attrs{"code": code},
  455. })
  456. if errors.Is(err, ErrIQGone) {
  457. return types.EmptyJID, wrapIQError(ErrInviteLinkRevoked, err)
  458. } else if errors.Is(err, ErrIQNotAcceptable) {
  459. return types.EmptyJID, wrapIQError(ErrInviteLinkInvalid, err)
  460. } else if err != nil {
  461. return types.EmptyJID, err
  462. }
  463. membershipApprovalModeNode, ok := resp.GetOptionalChildByTag("membership_approval_request")
  464. if ok {
  465. return membershipApprovalModeNode.AttrGetter().JID("jid"), nil
  466. }
  467. groupNode, ok := resp.GetOptionalChildByTag("group")
  468. if !ok {
  469. return types.EmptyJID, &ElementMissingError{Tag: "group", In: "response to group link join query"}
  470. }
  471. return groupNode.AttrGetter().JID("jid"), nil
  472. }
  473. // GetJoinedGroups returns the list of groups the user is participating in.
  474. func (cli *Client) GetJoinedGroups(ctx context.Context) ([]*types.GroupInfo, error) {
  475. resp, err := cli.sendGroupIQ(ctx, iqGet, types.GroupServerJID, waBinary.Node{
  476. Tag: "participating",
  477. Content: []waBinary.Node{
  478. {Tag: "participants"},
  479. {Tag: "description"},
  480. },
  481. })
  482. if err != nil {
  483. return nil, err
  484. }
  485. groups, ok := resp.GetOptionalChildByTag("groups")
  486. if !ok {
  487. return nil, &ElementMissingError{Tag: "groups", In: "response to group list query"}
  488. }
  489. children := groups.GetChildren()
  490. infos := make([]*types.GroupInfo, 0, len(children))
  491. var allLIDPairs []store.LIDMapping
  492. var allRedactedPhones []store.RedactedPhoneEntry
  493. for _, child := range children {
  494. if child.Tag != "group" {
  495. cli.Log.Debugf("Unexpected child in group list response: %s", child.XMLString())
  496. continue
  497. }
  498. parsed, parseErr := cli.parseGroupNode(&child)
  499. if parseErr != nil {
  500. cli.Log.Warnf("Error parsing group %s: %v", parsed.JID, parseErr)
  501. }
  502. lidPairs, redactedPhones := cli.cacheGroupInfo(parsed, true)
  503. allLIDPairs = append(allLIDPairs, lidPairs...)
  504. allRedactedPhones = append(allRedactedPhones, redactedPhones...)
  505. infos = append(infos, parsed)
  506. }
  507. err = cli.Store.LIDs.PutManyLIDMappings(ctx, allLIDPairs)
  508. if err != nil {
  509. cli.Log.Warnf("Failed to store LID mappings from joined groups: %v", err)
  510. }
  511. err = cli.Store.Contacts.PutManyRedactedPhones(ctx, allRedactedPhones)
  512. if err != nil {
  513. cli.Log.Warnf("Failed to store redacted phones from joined groups: %v", err)
  514. }
  515. return infos, nil
  516. }
  517. // GetSubGroups gets the subgroups of the given community.
  518. func (cli *Client) GetSubGroups(ctx context.Context, community types.JID) ([]*types.GroupLinkTarget, error) {
  519. res, err := cli.sendGroupIQ(ctx, iqGet, community, waBinary.Node{Tag: "sub_groups"})
  520. if err != nil {
  521. return nil, err
  522. }
  523. groups, ok := res.GetOptionalChildByTag("sub_groups")
  524. if !ok {
  525. return nil, &ElementMissingError{Tag: "sub_groups", In: "response to subgroups query"}
  526. }
  527. var parsedGroups []*types.GroupLinkTarget
  528. for _, child := range groups.GetChildren() {
  529. if child.Tag == "group" {
  530. parsedGroup, err := parseGroupLinkTargetNode(&child)
  531. if err != nil {
  532. return parsedGroups, fmt.Errorf("failed to parse group in subgroups list: %w", err)
  533. }
  534. parsedGroups = append(parsedGroups, &parsedGroup)
  535. }
  536. }
  537. return parsedGroups, nil
  538. }
  539. // GetLinkedGroupsParticipants gets all the participants in the groups of the given community.
  540. func (cli *Client) GetLinkedGroupsParticipants(ctx context.Context, community types.JID) ([]types.JID, error) {
  541. res, err := cli.sendGroupIQ(ctx, iqGet, community, waBinary.Node{Tag: "linked_groups_participants"})
  542. if err != nil {
  543. return nil, err
  544. }
  545. participants, ok := res.GetOptionalChildByTag("linked_groups_participants")
  546. if !ok {
  547. return nil, &ElementMissingError{Tag: "linked_groups_participants", In: "response to community participants query"}
  548. }
  549. members, lidPairs := parseParticipantList(&participants)
  550. if len(lidPairs) > 0 {
  551. err = cli.Store.LIDs.PutManyLIDMappings(ctx, lidPairs)
  552. if err != nil {
  553. cli.Log.Warnf("Failed to store LID mappings for community participants: %v", err)
  554. }
  555. }
  556. return members, nil
  557. }
  558. // GetGroupInfo requests basic info about a group chat from the WhatsApp servers.
  559. func (cli *Client) GetGroupInfo(ctx context.Context, jid types.JID) (*types.GroupInfo, error) {
  560. return cli.getGroupInfo(ctx, jid, true)
  561. }
  562. func (cli *Client) cacheGroupInfo(groupInfo *types.GroupInfo, lock bool) ([]store.LIDMapping, []store.RedactedPhoneEntry) {
  563. participants := make([]types.JID, len(groupInfo.Participants))
  564. lidPairs := make([]store.LIDMapping, len(groupInfo.Participants))
  565. redactedPhones := make([]store.RedactedPhoneEntry, 0)
  566. for i, part := range groupInfo.Participants {
  567. participants[i] = part.JID
  568. if !part.PhoneNumber.IsEmpty() && !part.LID.IsEmpty() {
  569. lidPairs[i] = store.LIDMapping{
  570. LID: part.LID,
  571. PN: part.PhoneNumber,
  572. }
  573. }
  574. if part.DisplayName != "" && !part.LID.IsEmpty() {
  575. redactedPhones = append(redactedPhones, store.RedactedPhoneEntry{
  576. JID: part.LID,
  577. RedactedPhone: part.DisplayName,
  578. })
  579. }
  580. }
  581. if lock {
  582. cli.groupCacheLock.Lock()
  583. defer cli.groupCacheLock.Unlock()
  584. }
  585. cli.groupCache[groupInfo.JID] = &groupMetaCache{
  586. AddressingMode: groupInfo.AddressingMode,
  587. CommunityAnnouncementGroup: groupInfo.IsAnnounce && groupInfo.IsDefaultSubGroup,
  588. Members: participants,
  589. }
  590. return lidPairs, redactedPhones
  591. }
  592. func (cli *Client) getGroupInfo(ctx context.Context, jid types.JID, lockParticipantCache bool) (*types.GroupInfo, error) {
  593. res, err := cli.sendGroupIQ(ctx, iqGet, jid, waBinary.Node{
  594. Tag: "query",
  595. Attrs: waBinary.Attrs{"request": "interactive"},
  596. })
  597. if errors.Is(err, ErrIQNotFound) {
  598. return nil, wrapIQError(ErrGroupNotFound, err)
  599. } else if errors.Is(err, ErrIQForbidden) {
  600. return nil, wrapIQError(ErrNotInGroup, err)
  601. } else if err != nil {
  602. return nil, err
  603. }
  604. groupNode, ok := res.GetOptionalChildByTag("group")
  605. if !ok {
  606. return nil, &ElementMissingError{Tag: "groups", In: "response to group info query"}
  607. }
  608. groupInfo, err := cli.parseGroupNode(&groupNode)
  609. if err != nil {
  610. return groupInfo, err
  611. }
  612. lidPairs, redactedPhones := cli.cacheGroupInfo(groupInfo, lockParticipantCache)
  613. err = cli.Store.LIDs.PutManyLIDMappings(ctx, lidPairs)
  614. if err != nil {
  615. cli.Log.Warnf("Failed to store LID mappings for members of %s: %v", jid, err)
  616. }
  617. err = cli.Store.Contacts.PutManyRedactedPhones(ctx, redactedPhones)
  618. if err != nil {
  619. cli.Log.Warnf("Failed to store redacted phones for members of %s: %v", jid, err)
  620. }
  621. return groupInfo, nil
  622. }
  623. func (cli *Client) getCachedGroupData(ctx context.Context, jid types.JID) (*groupMetaCache, error) {
  624. cli.groupCacheLock.Lock()
  625. defer cli.groupCacheLock.Unlock()
  626. if val, ok := cli.groupCache[jid]; ok {
  627. return val, nil
  628. }
  629. _, err := cli.getGroupInfo(ctx, jid, false)
  630. if err != nil {
  631. return nil, err
  632. }
  633. return cli.groupCache[jid], nil
  634. }
  635. func parseParticipant(childAG *waBinary.AttrUtility, child *waBinary.Node) types.GroupParticipant {
  636. pcpType := childAG.OptionalString("type")
  637. participant := types.GroupParticipant{
  638. IsAdmin: pcpType == "admin" || pcpType == "superadmin",
  639. IsSuperAdmin: pcpType == "superadmin",
  640. JID: childAG.JID("jid"),
  641. DisplayName: childAG.OptionalString("display_name"),
  642. }
  643. if participant.JID.Server == types.HiddenUserServer {
  644. participant.LID = participant.JID
  645. participant.PhoneNumber = childAG.OptionalJIDOrEmpty("phone_number")
  646. } else if participant.JID.Server == types.DefaultUserServer {
  647. participant.PhoneNumber = participant.JID
  648. participant.LID = childAG.OptionalJIDOrEmpty("lid")
  649. }
  650. if errorCode := childAG.OptionalInt("error"); errorCode != 0 {
  651. participant.Error = errorCode
  652. addRequest, ok := child.GetOptionalChildByTag("add_request")
  653. if ok {
  654. addAG := addRequest.AttrGetter()
  655. participant.AddRequest = &types.GroupParticipantAddRequest{
  656. Code: addAG.String("code"),
  657. Expiration: addAG.UnixTime("expiration"),
  658. }
  659. }
  660. }
  661. return participant
  662. }
  663. func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, error) {
  664. var group types.GroupInfo
  665. ag := groupNode.AttrGetter()
  666. group.JID = types.NewJID(ag.String("id"), types.GroupServer)
  667. group.OwnerJID = ag.OptionalJIDOrEmpty("creator")
  668. group.OwnerPN = ag.OptionalJIDOrEmpty("creator_pn")
  669. group.Name = ag.OptionalString("subject")
  670. group.NameSetAt = ag.OptionalUnixTime("s_t")
  671. group.NameSetBy = ag.OptionalJIDOrEmpty("s_o")
  672. group.NameSetByPN = ag.OptionalJIDOrEmpty("s_o_pn")
  673. group.GroupCreated = ag.UnixTime("creation")
  674. group.CreatorCountryCode = ag.OptionalString("creator_country_code")
  675. group.AnnounceVersionID = ag.OptionalString("a_v_id")
  676. group.ParticipantVersionID = ag.OptionalString("p_v_id")
  677. group.ParticipantCount = ag.OptionalInt("size")
  678. group.AddressingMode = types.AddressingMode(ag.OptionalString("addressing_mode"))
  679. for _, child := range groupNode.GetChildren() {
  680. childAG := child.AttrGetter()
  681. switch child.Tag {
  682. case "participant":
  683. group.Participants = append(group.Participants, parseParticipant(childAG, &child))
  684. case "description":
  685. body, bodyOK := child.GetOptionalChildByTag("body")
  686. if bodyOK {
  687. topicBytes, _ := body.Content.([]byte)
  688. group.Topic = string(topicBytes)
  689. group.TopicID = childAG.String("id")
  690. group.TopicSetBy = childAG.OptionalJIDOrEmpty("participant")
  691. group.TopicSetByPN = childAG.OptionalJIDOrEmpty("participant_pn") // TODO confirm field name
  692. group.TopicSetAt = childAG.UnixTime("t")
  693. }
  694. case "announcement":
  695. group.IsAnnounce = true
  696. case "locked":
  697. group.IsLocked = true
  698. case "ephemeral":
  699. group.IsEphemeral = true
  700. group.DisappearingTimer = uint32(childAG.Uint64("expiration"))
  701. case "member_add_mode":
  702. modeBytes, _ := child.Content.([]byte)
  703. group.MemberAddMode = types.GroupMemberAddMode(modeBytes)
  704. case "linked_parent":
  705. group.LinkedParentJID = childAG.JID("jid")
  706. case "default_sub_group":
  707. group.IsDefaultSubGroup = true
  708. case "parent":
  709. group.IsParent = true
  710. group.DefaultMembershipApprovalMode = childAG.OptionalString("default_membership_approval_mode")
  711. case "incognito":
  712. group.IsIncognito = true
  713. case "membership_approval_mode":
  714. group.IsJoinApprovalRequired = true
  715. case "suspended":
  716. group.Suspended = true
  717. default:
  718. cli.Log.Debugf("Unknown element in group node %s: %s", group.JID.String(), child.XMLString())
  719. }
  720. if !childAG.OK() {
  721. cli.Log.Warnf("Possibly failed to parse %s element in group node: %+v", child.Tag, childAG.Errors)
  722. }
  723. }
  724. return &group, ag.Error()
  725. }
  726. func parseGroupLinkTargetNode(groupNode *waBinary.Node) (types.GroupLinkTarget, error) {
  727. ag := groupNode.AttrGetter()
  728. jidKey := ag.OptionalJIDOrEmpty("jid")
  729. if jidKey.IsEmpty() {
  730. jidKey = types.NewJID(ag.String("id"), types.GroupServer)
  731. }
  732. return types.GroupLinkTarget{
  733. JID: jidKey,
  734. GroupName: types.GroupName{
  735. Name: ag.OptionalString("subject"),
  736. NameSetAt: ag.OptionalUnixTime("s_t"),
  737. },
  738. GroupIsDefaultSub: types.GroupIsDefaultSub{
  739. IsDefaultSubGroup: groupNode.GetChildByTag("default_sub_group").Tag == "default_sub_group",
  740. },
  741. }, ag.Error()
  742. }
  743. func parseParticipantList(node *waBinary.Node) (participants []types.JID, lidPairs []store.LIDMapping) {
  744. children := node.GetChildren()
  745. participants = make([]types.JID, 0, len(children))
  746. for _, child := range children {
  747. jid, ok := child.Attrs["jid"].(types.JID)
  748. if child.Tag != "participant" || !ok {
  749. continue
  750. }
  751. participants = append(participants, jid)
  752. if jid.Server == types.HiddenUserServer {
  753. phoneNumber, ok := child.Attrs["phone_number"].(types.JID)
  754. if ok && !phoneNumber.IsEmpty() {
  755. lidPairs = append(lidPairs, store.LIDMapping{
  756. LID: jid,
  757. PN: phoneNumber,
  758. })
  759. }
  760. } else if jid.Server == types.DefaultUserServer {
  761. lid, ok := child.Attrs["lid"].(types.JID)
  762. if ok && !lid.IsEmpty() {
  763. lidPairs = append(lidPairs, store.LIDMapping{
  764. LID: lid,
  765. PN: jid,
  766. })
  767. }
  768. }
  769. }
  770. return
  771. }
  772. func (cli *Client) parseGroupCreate(parentNode, node *waBinary.Node) (*events.JoinedGroup, []store.LIDMapping, []store.RedactedPhoneEntry, error) {
  773. groupNode, ok := node.GetOptionalChildByTag("group")
  774. if !ok {
  775. return nil, nil, nil, fmt.Errorf("group create notification didn't contain group info")
  776. }
  777. var evt events.JoinedGroup
  778. pag := parentNode.AttrGetter()
  779. ag := node.AttrGetter()
  780. evt.Reason = ag.OptionalString("reason")
  781. evt.CreateKey = ag.OptionalString("key")
  782. evt.Type = ag.OptionalString("type")
  783. evt.Sender = pag.OptionalJID("participant")
  784. evt.SenderPN = pag.OptionalJID("participant_pn")
  785. evt.Notify = pag.OptionalString("notify")
  786. info, err := cli.parseGroupNode(&groupNode)
  787. if err != nil {
  788. return nil, nil, nil, fmt.Errorf("failed to parse group info in create notification: %w", err)
  789. }
  790. evt.GroupInfo = *info
  791. lidPairs, redactedPhones := cli.cacheGroupInfo(info, true)
  792. return &evt, lidPairs, redactedPhones, nil
  793. }
  794. func (cli *Client) parseGroupChange(node *waBinary.Node) (*events.GroupInfo, []store.LIDMapping, error) {
  795. var evt events.GroupInfo
  796. ag := node.AttrGetter()
  797. evt.JID = ag.JID("from")
  798. evt.Notify = ag.OptionalString("notify")
  799. evt.Sender = ag.OptionalJID("participant")
  800. evt.SenderPN = ag.OptionalJID("participant_pn")
  801. evt.Timestamp = ag.UnixTime("t")
  802. if !ag.OK() {
  803. return nil, nil, fmt.Errorf("group change doesn't contain required attributes: %w", ag.Error())
  804. }
  805. var lidPairs []store.LIDMapping
  806. for _, child := range node.GetChildren() {
  807. cag := child.AttrGetter()
  808. if child.Tag == "add" || child.Tag == "remove" || child.Tag == "promote" || child.Tag == "demote" {
  809. evt.PrevParticipantVersionID = cag.OptionalString("prev_v_id")
  810. evt.ParticipantVersionID = cag.OptionalString("v_id")
  811. }
  812. switch child.Tag {
  813. case "add":
  814. evt.JoinReason = cag.OptionalString("reason")
  815. evt.Join, lidPairs = parseParticipantList(&child)
  816. case "remove":
  817. evt.Leave, lidPairs = parseParticipantList(&child)
  818. case "promote":
  819. evt.Promote, lidPairs = parseParticipantList(&child)
  820. case "demote":
  821. evt.Demote, lidPairs = parseParticipantList(&child)
  822. case "locked":
  823. evt.Locked = &types.GroupLocked{IsLocked: true}
  824. case "unlocked":
  825. evt.Locked = &types.GroupLocked{IsLocked: false}
  826. case "delete":
  827. evt.Delete = &types.GroupDelete{Deleted: true, DeleteReason: cag.String("reason")}
  828. case "subject":
  829. evt.Name = &types.GroupName{
  830. Name: cag.String("subject"),
  831. NameSetAt: cag.UnixTime("s_t"),
  832. NameSetBy: cag.OptionalJIDOrEmpty("s_o"),
  833. NameSetByPN: cag.OptionalJIDOrEmpty("s_o_pn"),
  834. }
  835. case "description":
  836. var topicStr string
  837. _, isDelete := child.GetOptionalChildByTag("delete")
  838. if !isDelete {
  839. topicChild := child.GetChildByTag("body")
  840. topicBytes, ok := topicChild.Content.([]byte)
  841. if !ok {
  842. return nil, nil, fmt.Errorf("group change description has unexpected body: %s", topicChild.XMLString())
  843. }
  844. topicStr = string(topicBytes)
  845. }
  846. var setBy types.JID
  847. if evt.Sender != nil {
  848. setBy = *evt.Sender
  849. }
  850. evt.Topic = &types.GroupTopic{
  851. Topic: topicStr,
  852. TopicID: cag.String("id"),
  853. TopicSetAt: evt.Timestamp,
  854. TopicSetBy: setBy,
  855. TopicDeleted: isDelete,
  856. }
  857. case "announcement":
  858. evt.Announce = &types.GroupAnnounce{
  859. IsAnnounce: true,
  860. AnnounceVersionID: cag.String("v_id"),
  861. }
  862. case "not_announcement":
  863. evt.Announce = &types.GroupAnnounce{
  864. IsAnnounce: false,
  865. AnnounceVersionID: cag.String("v_id"),
  866. }
  867. case "invite":
  868. link := InviteLinkPrefix + cag.String("code")
  869. evt.NewInviteLink = &link
  870. case "ephemeral":
  871. timer := uint32(cag.Uint64("expiration"))
  872. evt.Ephemeral = &types.GroupEphemeral{
  873. IsEphemeral: true,
  874. DisappearingTimer: timer,
  875. }
  876. case "not_ephemeral":
  877. evt.Ephemeral = &types.GroupEphemeral{IsEphemeral: false}
  878. case "link":
  879. evt.Link = &types.GroupLinkChange{
  880. Type: types.GroupLinkChangeType(cag.String("link_type")),
  881. }
  882. groupNode, ok := child.GetOptionalChildByTag("group")
  883. if !ok {
  884. return nil, nil, &ElementMissingError{Tag: "group", In: "group link"}
  885. }
  886. var err error
  887. evt.Link.Group, err = parseGroupLinkTargetNode(&groupNode)
  888. if err != nil {
  889. return nil, nil, fmt.Errorf("failed to parse group link node in group change: %w", err)
  890. }
  891. case "unlink":
  892. evt.Unlink = &types.GroupLinkChange{
  893. Type: types.GroupLinkChangeType(cag.String("unlink_type")),
  894. UnlinkReason: types.GroupUnlinkReason(cag.String("unlink_reason")),
  895. }
  896. groupNode, ok := child.GetOptionalChildByTag("group")
  897. if !ok {
  898. return nil, nil, &ElementMissingError{Tag: "group", In: "group unlink"}
  899. }
  900. var err error
  901. evt.Unlink.Group, err = parseGroupLinkTargetNode(&groupNode)
  902. if err != nil {
  903. return nil, nil, fmt.Errorf("failed to parse group unlink node in group change: %w", err)
  904. }
  905. case "membership_approval_mode":
  906. evt.MembershipApprovalMode = &types.GroupMembershipApprovalMode{
  907. IsJoinApprovalRequired: true,
  908. }
  909. case "suspended":
  910. evt.Suspended = true
  911. case "unsuspended":
  912. evt.Unsuspended = true
  913. default:
  914. evt.UnknownChanges = append(evt.UnknownChanges, &child)
  915. }
  916. if !cag.OK() {
  917. return nil, nil, fmt.Errorf("group change %s element doesn't contain required attributes: %w", child.Tag, cag.Error())
  918. }
  919. }
  920. return &evt, lidPairs, nil
  921. }
  922. func (cli *Client) updateGroupParticipantCache(evt *events.GroupInfo) {
  923. // TODO can the addressing mode change here?
  924. if len(evt.Join) == 0 && len(evt.Leave) == 0 {
  925. return
  926. }
  927. cli.groupCacheLock.Lock()
  928. defer cli.groupCacheLock.Unlock()
  929. cached, ok := cli.groupCache[evt.JID]
  930. if !ok {
  931. return
  932. }
  933. Outer:
  934. for _, jid := range evt.Join {
  935. for _, existingJID := range cached.Members {
  936. if jid == existingJID {
  937. continue Outer
  938. }
  939. }
  940. cached.Members = append(cached.Members, jid)
  941. }
  942. for _, jid := range evt.Leave {
  943. for i, existingJID := range cached.Members {
  944. if existingJID == jid {
  945. cached.Members[i] = cached.Members[len(cached.Members)-1]
  946. cached.Members = cached.Members[:len(cached.Members)-1]
  947. break
  948. }
  949. }
  950. }
  951. }
  952. func (cli *Client) parseGroupNotification(node *waBinary.Node) (any, []store.LIDMapping, []store.RedactedPhoneEntry, error) {
  953. children := node.GetChildren()
  954. if len(children) == 1 && children[0].Tag == "create" {
  955. return cli.parseGroupCreate(node, &children[0])
  956. } else {
  957. groupChange, lidPairs, err := cli.parseGroupChange(node)
  958. if err != nil {
  959. return nil, nil, nil, err
  960. }
  961. cli.updateGroupParticipantCache(groupChange)
  962. return groupChange, lidPairs, nil, nil
  963. }
  964. }
  965. // SetGroupJoinApprovalMode sets the group join approval mode to 'on' or 'off'.
  966. func (cli *Client) SetGroupJoinApprovalMode(ctx context.Context, jid types.JID, mode bool) error {
  967. modeStr := "off"
  968. if mode {
  969. modeStr = "on"
  970. }
  971. content := waBinary.Node{
  972. Tag: "membership_approval_mode",
  973. Content: []waBinary.Node{
  974. {
  975. Tag: "group_join",
  976. Attrs: waBinary.Attrs{"state": modeStr},
  977. },
  978. },
  979. }
  980. _, err := cli.sendGroupIQ(ctx, iqSet, jid, content)
  981. return err
  982. }
  983. // SetGroupMemberAddMode sets the group member add mode to 'admin_add' or 'all_member_add'.
  984. func (cli *Client) SetGroupMemberAddMode(ctx context.Context, jid types.JID, mode types.GroupMemberAddMode) error {
  985. if mode != types.GroupMemberAddModeAdmin && mode != types.GroupMemberAddModeAllMember {
  986. return errors.New("invalid mode, must be 'admin_add' or 'all_member_add'")
  987. }
  988. content := waBinary.Node{
  989. Tag: "member_add_mode",
  990. Content: []byte(mode),
  991. }
  992. _, err := cli.sendGroupIQ(ctx, iqSet, jid, content)
  993. return err
  994. }
  995. // SetGroupDescription updates the group description.
  996. func (cli *Client) SetGroupDescription(ctx context.Context, jid types.JID, description string) error {
  997. content := waBinary.Node{
  998. Tag: "description",
  999. Content: []waBinary.Node{
  1000. {
  1001. Tag: "body",
  1002. Content: []byte(description),
  1003. },
  1004. },
  1005. }
  1006. _, err := cli.sendGroupIQ(ctx, iqSet, jid, content)
  1007. return err
  1008. }