user.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  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. "encoding/base64"
  10. "errors"
  11. "fmt"
  12. "slices"
  13. "strings"
  14. "google.golang.org/protobuf/proto"
  15. waBinary "git.bobomao.top/joey/testwh/binary"
  16. "git.bobomao.top/joey/testwh/proto/waHistorySync"
  17. "git.bobomao.top/joey/testwh/proto/waVnameCert"
  18. "git.bobomao.top/joey/testwh/store"
  19. "git.bobomao.top/joey/testwh/types"
  20. "git.bobomao.top/joey/testwh/types/events"
  21. )
  22. const (
  23. BusinessMessageLinkPrefix = "https://wa.me/message/"
  24. ContactQRLinkPrefix = "https://wa.me/qr/"
  25. BusinessMessageLinkDirectPrefix = "https://api.whatsapp.com/message/"
  26. ContactQRLinkDirectPrefix = "https://api.whatsapp.com/qr/"
  27. NewsletterLinkPrefix = "https://whatsapp.com/channel/"
  28. )
  29. // ResolveBusinessMessageLink resolves a business message short link and returns the target JID, business name and
  30. // text to prefill in the input field (if any).
  31. //
  32. // The links look like https://wa.me/message/<code> or https://api.whatsapp.com/message/<code>. You can either provide
  33. // the full link, or just the <code> part.
  34. func (cli *Client) ResolveBusinessMessageLink(ctx context.Context, code string) (*types.BusinessMessageLinkTarget, error) {
  35. code = strings.TrimPrefix(code, BusinessMessageLinkPrefix)
  36. code = strings.TrimPrefix(code, BusinessMessageLinkDirectPrefix)
  37. resp, err := cli.sendIQ(ctx, infoQuery{
  38. Namespace: "w:qr",
  39. Type: iqGet,
  40. // WhatsApp android doesn't seem to have a "to" field for this one at all, not sure why but it works
  41. Content: []waBinary.Node{{
  42. Tag: "qr",
  43. Attrs: waBinary.Attrs{
  44. "code": code,
  45. },
  46. }},
  47. })
  48. if errors.Is(err, ErrIQNotFound) {
  49. return nil, wrapIQError(ErrBusinessMessageLinkNotFound, err)
  50. } else if err != nil {
  51. return nil, err
  52. }
  53. qrChild, ok := resp.GetOptionalChildByTag("qr")
  54. if !ok {
  55. return nil, &ElementMissingError{Tag: "qr", In: "response to business message link query"}
  56. }
  57. var target types.BusinessMessageLinkTarget
  58. ag := qrChild.AttrGetter()
  59. target.JID = ag.JID("jid")
  60. target.PushName = ag.String("notify")
  61. messageChild, ok := qrChild.GetOptionalChildByTag("message")
  62. if ok {
  63. messageBytes, _ := messageChild.Content.([]byte)
  64. target.Message = string(messageBytes)
  65. }
  66. businessChild, ok := qrChild.GetOptionalChildByTag("business")
  67. if ok {
  68. bag := businessChild.AttrGetter()
  69. target.IsSigned = bag.OptionalBool("is_signed")
  70. target.VerifiedName = bag.OptionalString("verified_name")
  71. target.VerifiedLevel = bag.OptionalString("verified_level")
  72. }
  73. return &target, ag.Error()
  74. }
  75. // ResolveContactQRLink resolves a link from a contact share QR code and returns the target JID and push name.
  76. //
  77. // The links look like https://wa.me/qr/<code> or https://api.whatsapp.com/qr/<code>. You can either provide
  78. // the full link, or just the <code> part.
  79. func (cli *Client) ResolveContactQRLink(ctx context.Context, code string) (*types.ContactQRLinkTarget, error) {
  80. code = strings.TrimPrefix(code, ContactQRLinkPrefix)
  81. code = strings.TrimPrefix(code, ContactQRLinkDirectPrefix)
  82. resp, err := cli.sendIQ(ctx, infoQuery{
  83. Namespace: "w:qr",
  84. Type: iqGet,
  85. Content: []waBinary.Node{{
  86. Tag: "qr",
  87. Attrs: waBinary.Attrs{
  88. "code": code,
  89. },
  90. }},
  91. })
  92. if errors.Is(err, ErrIQNotFound) {
  93. return nil, wrapIQError(ErrContactQRLinkNotFound, err)
  94. } else if err != nil {
  95. return nil, err
  96. }
  97. qrChild, ok := resp.GetOptionalChildByTag("qr")
  98. if !ok {
  99. return nil, &ElementMissingError{Tag: "qr", In: "response to contact link query"}
  100. }
  101. var target types.ContactQRLinkTarget
  102. ag := qrChild.AttrGetter()
  103. target.JID = ag.JID("jid")
  104. target.PushName = ag.OptionalString("notify")
  105. target.Type = ag.String("type")
  106. return &target, ag.Error()
  107. }
  108. // GetContactQRLink gets your own contact share QR link that can be resolved using ResolveContactQRLink
  109. // (or scanned with the official apps when encoded as a QR code).
  110. //
  111. // If the revoke parameter is set to true, it will ask the server to revoke the previous link and generate a new one.
  112. func (cli *Client) GetContactQRLink(ctx context.Context, revoke bool) (string, error) {
  113. action := "get"
  114. if revoke {
  115. action = "revoke"
  116. }
  117. resp, err := cli.sendIQ(ctx, infoQuery{
  118. Namespace: "w:qr",
  119. Type: iqSet,
  120. Content: []waBinary.Node{{
  121. Tag: "qr",
  122. Attrs: waBinary.Attrs{
  123. "type": "contact",
  124. "action": action,
  125. },
  126. }},
  127. })
  128. if err != nil {
  129. return "", err
  130. }
  131. qrChild, ok := resp.GetOptionalChildByTag("qr")
  132. if !ok {
  133. return "", &ElementMissingError{Tag: "qr", In: "response to own contact link fetch"}
  134. }
  135. ag := qrChild.AttrGetter()
  136. return ag.String("code"), ag.Error()
  137. }
  138. // SetStatusMessage updates the current user's status text, which is shown in the "About" section in the user profile.
  139. //
  140. // This is different from the ephemeral status broadcast messages. Use SendMessage to types.StatusBroadcastJID to send
  141. // such messages.
  142. func (cli *Client) SetStatusMessage(ctx context.Context, msg string) error {
  143. _, err := cli.sendIQ(ctx, infoQuery{
  144. Namespace: "status",
  145. Type: iqSet,
  146. To: types.ServerJID,
  147. Content: []waBinary.Node{{
  148. Tag: "status",
  149. Content: msg,
  150. }},
  151. })
  152. return err
  153. }
  154. func (cli *Client) Usync(ctx context.Context, phones []string) ([]types.IsOnWhatsAppResponse, error) {
  155. jids := make([]types.JID, len(phones))
  156. for i := range jids {
  157. jids[i] = types.NewJID(phones[i], types.LegacyUserServer)
  158. }
  159. list, err := cli.usync(ctx, jids, "delta", "interactive", []waBinary.Node{
  160. //{Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
  161. {Tag: "lid"},
  162. {Tag: "status"},
  163. {Tag: "contact"},
  164. })
  165. if err != nil {
  166. return nil, err
  167. }
  168. output := make([]types.IsOnWhatsAppResponse, 0, len(jids))
  169. querySuffix := "@" + types.LegacyUserServer
  170. for _, child := range list.GetChildren() {
  171. jid, jidOK := child.Attrs["jid"].(types.JID)
  172. if child.Tag != "user" || !jidOK {
  173. continue
  174. }
  175. var info types.IsOnWhatsAppResponse
  176. info.JID = jid
  177. info.VerifiedName, err = parseVerifiedName(child.GetChildByTag("business"))
  178. if err != nil {
  179. cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
  180. }
  181. status, _ := child.GetChildByTag("status").Content.([]byte)
  182. info.Status = string(status)
  183. contactNode := child.GetChildByTag("contact")
  184. info.IsIn = contactNode.AttrGetter().String("type") == "in"
  185. contactQuery, _ := contactNode.Content.([]byte)
  186. info.Query = strings.TrimSuffix(string(contactQuery), querySuffix)
  187. output = append(output, info)
  188. }
  189. return output, nil
  190. }
  191. // IsOnWhatsApp checks if the given phone numbers are registered on WhatsApp.
  192. // The phone numbers should be in international format, including the `+` prefix.
  193. func (cli *Client) IsOnWhatsApp(ctx context.Context, phones []string) ([]types.IsOnWhatsAppResponse, error) {
  194. jids := make([]types.JID, len(phones))
  195. for i := range jids {
  196. jids[i] = types.NewJID(phones[i], types.LegacyUserServer)
  197. }
  198. list, err := cli.usync(ctx, jids, "query", "interactive", []waBinary.Node{
  199. {Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
  200. {Tag: "contact"},
  201. })
  202. if err != nil {
  203. return nil, err
  204. }
  205. output := make([]types.IsOnWhatsAppResponse, 0, len(jids))
  206. querySuffix := "@" + types.LegacyUserServer
  207. for _, child := range list.GetChildren() {
  208. jid, jidOK := child.Attrs["jid"].(types.JID)
  209. if child.Tag != "user" || !jidOK {
  210. continue
  211. }
  212. var info types.IsOnWhatsAppResponse
  213. info.JID = jid
  214. info.VerifiedName, err = parseVerifiedName(child.GetChildByTag("business"))
  215. if err != nil {
  216. cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
  217. }
  218. contactNode := child.GetChildByTag("contact")
  219. info.IsIn = contactNode.AttrGetter().String("type") == "in"
  220. contactQuery, _ := contactNode.Content.([]byte)
  221. info.Query = strings.TrimSuffix(string(contactQuery), querySuffix)
  222. output = append(output, info)
  223. }
  224. return output, nil
  225. }
  226. // GetUserInfo gets basic user info (avatar, status, verified business name, device list).
  227. func (cli *Client) GetUserInfo(ctx context.Context, jids []types.JID) (map[types.JID]types.UserInfo, error) {
  228. list, err := cli.usync(ctx, jids, "full", "background", []waBinary.Node{
  229. {Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
  230. {Tag: "status"},
  231. {Tag: "picture"},
  232. {Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
  233. {Tag: "lid"},
  234. })
  235. if err != nil {
  236. return nil, err
  237. }
  238. respData := make(map[types.JID]types.UserInfo, len(jids))
  239. mappings := make([]store.LIDMapping, 0, len(jids))
  240. for _, child := range list.GetChildren() {
  241. jid, jidOK := child.Attrs["jid"].(types.JID)
  242. if child.Tag != "user" || !jidOK {
  243. continue
  244. }
  245. var info types.UserInfo
  246. verifiedName, err := parseVerifiedName(child.GetChildByTag("business"))
  247. if err != nil {
  248. cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
  249. }
  250. status, _ := child.GetChildByTag("status").Content.([]byte)
  251. info.Status = string(status)
  252. info.PictureID, _ = child.GetChildByTag("picture").Attrs["id"].(string)
  253. info.Devices = parseDeviceList(jid, child.GetChildByTag("devices"))
  254. lidTag := child.GetChildByTag("lid")
  255. info.LID = lidTag.AttrGetter().OptionalJIDOrEmpty("val")
  256. if !info.LID.IsEmpty() {
  257. mappings = append(mappings, store.LIDMapping{PN: jid, LID: info.LID})
  258. }
  259. if verifiedName != nil {
  260. cli.updateBusinessName(ctx, jid, info.LID, nil, verifiedName.Details.GetVerifiedName())
  261. }
  262. respData[jid] = info
  263. }
  264. err = cli.Store.LIDs.PutManyLIDMappings(ctx, mappings)
  265. if err != nil {
  266. // not worth returning on the error, instead just post a log
  267. cli.Log.Errorf("Failed to place LID mappings from USync call")
  268. }
  269. return respData, nil
  270. }
  271. func (cli *Client) GetBotListV2(ctx context.Context) ([]types.BotListInfo, error) {
  272. resp, err := cli.sendIQ(ctx, infoQuery{
  273. To: types.ServerJID,
  274. Namespace: "bot",
  275. Type: iqGet,
  276. Content: []waBinary.Node{
  277. {Tag: "bot", Attrs: waBinary.Attrs{"v": "2"}},
  278. },
  279. })
  280. if err != nil {
  281. return nil, err
  282. }
  283. botNode, ok := resp.GetOptionalChildByTag("bot")
  284. if !ok {
  285. return nil, &ElementMissingError{Tag: "bot", In: "response to bot list query"}
  286. }
  287. var list []types.BotListInfo
  288. for _, section := range botNode.GetChildrenByTag("section") {
  289. if section.AttrGetter().String("type") == "all" {
  290. for _, bot := range section.GetChildrenByTag("bot") {
  291. ag := bot.AttrGetter()
  292. list = append(list, types.BotListInfo{
  293. PersonaID: ag.String("persona_id"),
  294. BotJID: ag.JID("jid"),
  295. })
  296. }
  297. }
  298. }
  299. return list, nil
  300. }
  301. func (cli *Client) GetBotProfiles(ctx context.Context, botInfo []types.BotListInfo) ([]types.BotProfileInfo, error) {
  302. jids := make([]types.JID, len(botInfo))
  303. for i, bot := range botInfo {
  304. jids[i] = bot.BotJID
  305. }
  306. list, err := cli.usync(ctx, jids, "query", "interactive", []waBinary.Node{
  307. {Tag: "bot", Content: []waBinary.Node{{Tag: "profile", Attrs: waBinary.Attrs{"v": "1"}}}},
  308. }, UsyncQueryExtras{
  309. BotListInfo: botInfo,
  310. })
  311. if err != nil {
  312. return nil, err
  313. }
  314. var profiles []types.BotProfileInfo
  315. for _, user := range list.GetChildren() {
  316. jid := user.AttrGetter().JID("jid")
  317. bot := user.GetChildByTag("bot")
  318. profile := bot.GetChildByTag("profile")
  319. name := string(profile.GetChildByTag("name").Content.([]byte))
  320. attributes := string(profile.GetChildByTag("attributes").Content.([]byte))
  321. description := string(profile.GetChildByTag("description").Content.([]byte))
  322. category := string(profile.GetChildByTag("category").Content.([]byte))
  323. _, isDefault := profile.GetOptionalChildByTag("default")
  324. personaID := profile.AttrGetter().String("persona_id")
  325. commandsNode := profile.GetChildByTag("commands")
  326. commandDescription := string(commandsNode.GetChildByTag("description").Content.([]byte))
  327. var commands []types.BotProfileCommand
  328. for _, commandNode := range commandsNode.GetChildrenByTag("command") {
  329. commands = append(commands, types.BotProfileCommand{
  330. Name: string(commandNode.GetChildByTag("name").Content.([]byte)),
  331. Description: string(commandNode.GetChildByTag("description").Content.([]byte)),
  332. })
  333. }
  334. promptsNode := profile.GetChildByTag("prompts")
  335. var prompts []string
  336. for _, promptNode := range promptsNode.GetChildrenByTag("prompt") {
  337. prompts = append(
  338. prompts,
  339. fmt.Sprintf(
  340. "%s %s",
  341. string(promptNode.GetChildByTag("emoji").Content.([]byte)),
  342. string(promptNode.GetChildByTag("text").Content.([]byte)),
  343. ),
  344. )
  345. }
  346. profiles = append(profiles, types.BotProfileInfo{
  347. JID: jid,
  348. Name: name,
  349. Attributes: attributes,
  350. Description: description,
  351. Category: category,
  352. IsDefault: isDefault,
  353. Prompts: prompts,
  354. PersonaID: personaID,
  355. Commands: commands,
  356. CommandsDescription: commandDescription,
  357. })
  358. }
  359. return profiles, nil
  360. }
  361. func (cli *Client) parseBusinessProfile(node *waBinary.Node) (*types.BusinessProfile, error) {
  362. profileNode := node.GetChildByTag("profile")
  363. jid, ok := profileNode.AttrGetter().GetJID("jid", true)
  364. if !ok {
  365. return nil, errors.New("missing jid in business profile")
  366. }
  367. address, _ := profileNode.GetChildByTag("address").Content.([]byte)
  368. email, _ := profileNode.GetChildByTag("email").Content.([]byte)
  369. businessHour := profileNode.GetChildByTag("business_hours")
  370. businessHourTimezone := businessHour.AttrGetter().String("timezone")
  371. businessHoursConfigs := businessHour.GetChildren()
  372. businessHours := make([]types.BusinessHoursConfig, 0)
  373. for _, config := range businessHoursConfigs {
  374. if config.Tag != "business_hours_config" {
  375. continue
  376. }
  377. dow := config.AttrGetter().String("day_of_week")
  378. mode := config.AttrGetter().String("mode")
  379. openTime := config.AttrGetter().String("open_time")
  380. closeTime := config.AttrGetter().String("close_time")
  381. businessHours = append(businessHours, types.BusinessHoursConfig{
  382. DayOfWeek: dow,
  383. Mode: mode,
  384. OpenTime: openTime,
  385. CloseTime: closeTime,
  386. })
  387. }
  388. categoriesNode := profileNode.GetChildByTag("categories")
  389. categories := make([]types.Category, 0)
  390. for _, category := range categoriesNode.GetChildren() {
  391. if category.Tag != "category" {
  392. continue
  393. }
  394. id := category.AttrGetter().String("id")
  395. name, _ := category.Content.([]byte)
  396. categories = append(categories, types.Category{
  397. ID: id,
  398. Name: string(name),
  399. })
  400. }
  401. profileOptionsNode := profileNode.GetChildByTag("profile_options")
  402. profileOptions := make(map[string]string)
  403. for _, option := range profileOptionsNode.GetChildren() {
  404. optValueBytes, _ := option.Content.([]byte)
  405. profileOptions[option.Tag] = string(optValueBytes)
  406. // TODO parse bot_fields
  407. }
  408. return &types.BusinessProfile{
  409. JID: jid,
  410. Email: string(email),
  411. Address: string(address),
  412. Categories: categories,
  413. ProfileOptions: profileOptions,
  414. BusinessHoursTimeZone: businessHourTimezone,
  415. BusinessHours: businessHours,
  416. }, nil
  417. }
  418. // GetBusinessProfile gets the profile info of a WhatsApp business account
  419. func (cli *Client) GetBusinessProfile(ctx context.Context, jid types.JID) (*types.BusinessProfile, error) {
  420. resp, err := cli.sendIQ(ctx, infoQuery{
  421. Type: iqGet,
  422. To: types.ServerJID,
  423. Namespace: "w:biz",
  424. Content: []waBinary.Node{{
  425. Tag: "business_profile",
  426. Attrs: waBinary.Attrs{
  427. "v": "244",
  428. },
  429. Content: []waBinary.Node{{
  430. Tag: "profile",
  431. Attrs: waBinary.Attrs{
  432. "jid": jid,
  433. },
  434. }},
  435. }},
  436. })
  437. if err != nil {
  438. return nil, err
  439. }
  440. node, ok := resp.GetOptionalChildByTag("business_profile")
  441. if !ok {
  442. return nil, &ElementMissingError{Tag: "business_profile", In: "response to business profile query"}
  443. }
  444. return cli.parseBusinessProfile(&node)
  445. }
  446. func (cli *Client) GetUserDevicesContext(ctx context.Context, jids []types.JID) ([]types.JID, error) {
  447. return cli.GetUserDevices(ctx, jids)
  448. }
  449. // GetUserDevices gets the list of devices that the given user has. The input should be a list of
  450. // regular JIDs, and the output will be a list of AD JIDs. The local device will not be included in
  451. // the output even if the user's JID is included in the input. All other devices will be included.
  452. func (cli *Client) GetUserDevices(ctx context.Context, jids []types.JID) ([]types.JID, error) {
  453. if cli == nil {
  454. return nil, ErrClientIsNil
  455. }
  456. cli.userDevicesCacheLock.Lock()
  457. defer cli.userDevicesCacheLock.Unlock()
  458. var devices, jidsToSync, fbJIDsToSync []types.JID
  459. for _, jid := range jids {
  460. cached, ok := cli.userDevicesCache[jid]
  461. if ok && len(cached.devices) > 0 {
  462. devices = append(devices, cached.devices...)
  463. } else if jid.Server == types.MessengerServer {
  464. fbJIDsToSync = append(fbJIDsToSync, jid)
  465. } else if jid.IsBot() {
  466. // Bot JIDs do not have devices, the usync query is empty
  467. devices = append(devices, jid)
  468. } else {
  469. jidsToSync = append(jidsToSync, jid)
  470. }
  471. }
  472. if len(jidsToSync) > 0 {
  473. list, err := cli.usync(ctx, jidsToSync, "query", "message", []waBinary.Node{
  474. {Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
  475. })
  476. if err != nil {
  477. return nil, err
  478. }
  479. for _, user := range list.GetChildren() {
  480. jid, jidOK := user.Attrs["jid"].(types.JID)
  481. if user.Tag != "user" || !jidOK {
  482. continue
  483. }
  484. userDevices := parseDeviceList(jid, user.GetChildByTag("devices"))
  485. cli.userDevicesCache[jid] = deviceCache{devices: userDevices, dhash: participantListHashV2(userDevices)}
  486. devices = append(devices, userDevices...)
  487. }
  488. }
  489. if len(fbJIDsToSync) > 0 {
  490. userDevices, err := cli.getFBIDDevices(ctx, fbJIDsToSync)
  491. if err != nil {
  492. return nil, err
  493. }
  494. devices = append(devices, userDevices...)
  495. }
  496. return devices, nil
  497. }
  498. type GetProfilePictureParams struct {
  499. Preview bool
  500. ExistingID string
  501. IsCommunity bool
  502. // This is a common group ID that you share with the target
  503. CommonGID types.JID
  504. // use this to query the profile photo of a group you don't have joined, but you have an invite code for
  505. InviteCode string
  506. // Persona ID when getting profile of Meta AI bots
  507. PersonaID string
  508. }
  509. // GetProfilePictureInfo gets the URL where you can download a WhatsApp user's profile picture or group's photo.
  510. //
  511. // Optionally, you can pass the last known profile picture ID.
  512. // If the profile picture hasn't changed, this will return nil with no error.
  513. //
  514. // To get a community photo, you should pass `IsCommunity: true`, as otherwise you may get a 401 error.
  515. func (cli *Client) GetProfilePictureInfo(ctx context.Context, jid types.JID, params *GetProfilePictureParams) (*types.ProfilePictureInfo, error) {
  516. if cli == nil {
  517. return nil, ErrClientIsNil
  518. }
  519. attrs := waBinary.Attrs{
  520. "query": "url",
  521. }
  522. var target, to types.JID
  523. if params == nil {
  524. params = &GetProfilePictureParams{}
  525. }
  526. if params.Preview {
  527. attrs["type"] = "preview"
  528. } else {
  529. attrs["type"] = "image"
  530. }
  531. if params.ExistingID != "" {
  532. attrs["id"] = params.ExistingID
  533. }
  534. if params.InviteCode != "" {
  535. attrs["invite"] = params.InviteCode
  536. }
  537. var expectWrapped bool
  538. var content []waBinary.Node
  539. namespace := "w:profile:picture"
  540. if params.IsCommunity {
  541. target = types.EmptyJID
  542. namespace = "w:g2"
  543. to = jid
  544. attrs["parent_group_jid"] = jid
  545. expectWrapped = true
  546. content = []waBinary.Node{{
  547. Tag: "pictures",
  548. Content: []waBinary.Node{{
  549. Tag: "picture",
  550. Attrs: attrs,
  551. }},
  552. }}
  553. } else {
  554. to = types.ServerJID
  555. target = jid
  556. if !params.CommonGID.IsEmpty() {
  557. attrs["common_gid"] = params.CommonGID
  558. }
  559. if params.PersonaID != "" {
  560. attrs["persona_id"] = params.PersonaID
  561. }
  562. var pictureContent []waBinary.Node
  563. if token, _ := cli.Store.PrivacyTokens.GetPrivacyToken(ctx, jid); token != nil {
  564. pictureContent = []waBinary.Node{{
  565. Tag: "tctoken",
  566. Content: token.Token,
  567. }}
  568. }
  569. content = []waBinary.Node{{
  570. Tag: "picture",
  571. Attrs: attrs,
  572. Content: pictureContent,
  573. }}
  574. }
  575. resp, err := cli.sendIQ(ctx, infoQuery{
  576. Namespace: namespace,
  577. Type: "get",
  578. To: to,
  579. Target: target,
  580. Content: content,
  581. })
  582. if errors.Is(err, ErrIQNotAuthorized) {
  583. return nil, wrapIQError(ErrProfilePictureUnauthorized, err)
  584. } else if errors.Is(err, ErrIQNotFound) {
  585. return nil, wrapIQError(ErrProfilePictureNotSet, err)
  586. } else if err != nil {
  587. return nil, err
  588. }
  589. if expectWrapped {
  590. pics, ok := resp.GetOptionalChildByTag("pictures")
  591. if !ok {
  592. return nil, &ElementMissingError{Tag: "pictures", In: "response to profile picture query"}
  593. }
  594. resp = &pics
  595. }
  596. picture, ok := resp.GetOptionalChildByTag("picture")
  597. if !ok {
  598. if params.ExistingID != "" {
  599. return nil, nil
  600. }
  601. return nil, &ElementMissingError{Tag: "picture", In: "response to profile picture query"}
  602. }
  603. var info types.ProfilePictureInfo
  604. ag := picture.AttrGetter()
  605. if ag.OptionalInt("status") == 304 {
  606. return nil, nil
  607. } else if ag.OptionalInt("status") == 204 {
  608. return nil, ErrProfilePictureNotSet
  609. }
  610. info.ID = ag.String("id")
  611. info.URL = ag.String("url")
  612. info.Type = ag.String("type")
  613. info.DirectPath = ag.String("direct_path")
  614. info.Hash, _ = base64.StdEncoding.DecodeString(ag.OptionalString("hash"))
  615. if !ag.OK() {
  616. return &info, ag.Error()
  617. }
  618. return &info, nil
  619. }
  620. func (cli *Client) handleHistoricalPushNames(ctx context.Context, names []*waHistorySync.Pushname) {
  621. if cli.Store.Contacts == nil {
  622. return
  623. }
  624. cli.Log.Infof("Updating contact store with %d push names from history sync", len(names))
  625. for _, user := range names {
  626. if user.GetPushname() == "-" {
  627. continue
  628. }
  629. var changed bool
  630. if jid, err := types.ParseJID(user.GetID()); err != nil {
  631. cli.Log.Warnf("Failed to parse user ID '%s' in push name history sync: %v", user.GetID(), err)
  632. } else if changed, _, err = cli.Store.Contacts.PutPushName(ctx, jid, user.GetPushname()); err != nil {
  633. cli.Log.Warnf("Failed to store push name of %s from history sync: %v", jid, err)
  634. } else if changed {
  635. cli.Log.Debugf("Got push name %s for %s in history sync", user.GetPushname(), jid)
  636. }
  637. }
  638. }
  639. func (cli *Client) updatePushName(ctx context.Context, user, userAlt types.JID, messageInfo *types.MessageInfo, name string) {
  640. if cli.Store.Contacts == nil {
  641. return
  642. }
  643. user = user.ToNonAD()
  644. changed, previousName, err := cli.Store.Contacts.PutPushName(ctx, user, name)
  645. if err != nil {
  646. cli.Log.Errorf("Failed to save push name of %s in device store: %v", user, err)
  647. } else if changed {
  648. userAlt = userAlt.ToNonAD()
  649. if userAlt.IsEmpty() {
  650. userAlt, _ = cli.Store.GetAltJID(ctx, user)
  651. }
  652. if !userAlt.IsEmpty() {
  653. _, _, err = cli.Store.Contacts.PutPushName(ctx, userAlt, name)
  654. if err != nil {
  655. cli.Log.Errorf("Failed to save push name of %s in device store: %v", userAlt, err)
  656. }
  657. }
  658. cli.Log.Debugf("Push name of %s changed from %s to %s, dispatching event", user, previousName, name)
  659. cli.dispatchEvent(&events.PushName{
  660. JID: user,
  661. JIDAlt: userAlt,
  662. Message: messageInfo,
  663. OldPushName: previousName,
  664. NewPushName: name,
  665. })
  666. }
  667. }
  668. func (cli *Client) updateBusinessName(ctx context.Context, user, userAlt types.JID, messageInfo *types.MessageInfo, name string) {
  669. if cli.Store.Contacts == nil {
  670. return
  671. }
  672. changed, previousName, err := cli.Store.Contacts.PutBusinessName(ctx, user, name)
  673. if err != nil {
  674. cli.Log.Errorf("Failed to save business name of %s in device store: %v", user, err)
  675. } else if changed {
  676. userAlt = userAlt.ToNonAD()
  677. if userAlt.IsEmpty() {
  678. userAlt, _ = cli.Store.GetAltJID(ctx, user)
  679. }
  680. if !userAlt.IsEmpty() {
  681. _, _, err = cli.Store.Contacts.PutBusinessName(ctx, userAlt, name)
  682. if err != nil {
  683. cli.Log.Errorf("Failed to save push name of %s in device store: %v", userAlt, err)
  684. }
  685. }
  686. cli.Log.Debugf("Business name of %s changed from %s to %s, dispatching event", user, previousName, name)
  687. cli.dispatchEvent(&events.BusinessName{
  688. JID: user,
  689. Message: messageInfo,
  690. OldBusinessName: previousName,
  691. NewBusinessName: name,
  692. })
  693. }
  694. }
  695. func parseVerifiedName(businessNode waBinary.Node) (*types.VerifiedName, error) {
  696. if businessNode.Tag != "business" {
  697. return nil, nil
  698. }
  699. verifiedNameNode, ok := businessNode.GetOptionalChildByTag("verified_name")
  700. if !ok {
  701. return nil, nil
  702. }
  703. return parseVerifiedNameContent(verifiedNameNode)
  704. }
  705. func parseVerifiedNameContent(verifiedNameNode waBinary.Node) (*types.VerifiedName, error) {
  706. rawCert, ok := verifiedNameNode.Content.([]byte)
  707. if !ok {
  708. return nil, nil
  709. }
  710. var cert waVnameCert.VerifiedNameCertificate
  711. err := proto.Unmarshal(rawCert, &cert)
  712. if err != nil {
  713. return nil, err
  714. }
  715. var certDetails waVnameCert.VerifiedNameCertificate_Details
  716. err = proto.Unmarshal(cert.GetDetails(), &certDetails)
  717. if err != nil {
  718. return nil, err
  719. }
  720. return &types.VerifiedName{
  721. Certificate: &cert,
  722. Details: &certDetails,
  723. }, nil
  724. }
  725. func parseDeviceList(user types.JID, deviceNode waBinary.Node) []types.JID {
  726. deviceList := deviceNode.GetChildByTag("device-list")
  727. if deviceNode.Tag != "devices" || deviceList.Tag != "device-list" {
  728. return nil
  729. }
  730. children := deviceList.GetChildren()
  731. devices := make([]types.JID, 0, len(children))
  732. for _, device := range children {
  733. deviceID, ok := device.AttrGetter().GetInt64("id", true)
  734. isHosted := device.AttrGetter().Bool("is_hosted")
  735. if device.Tag != "device" || !ok {
  736. continue
  737. }
  738. user.Device = uint16(deviceID)
  739. if isHosted {
  740. hostedUser := user
  741. if user.Server == types.HiddenUserServer {
  742. hostedUser.Server = types.HostedLIDServer
  743. } else {
  744. hostedUser.Server = types.HostedServer
  745. }
  746. devices = append(devices, hostedUser)
  747. } else {
  748. devices = append(devices, user)
  749. }
  750. }
  751. return devices
  752. }
  753. func parseFBDeviceList(user types.JID, deviceList waBinary.Node) deviceCache {
  754. children := deviceList.GetChildren()
  755. devices := make([]types.JID, 0, len(children))
  756. for _, device := range children {
  757. deviceID, ok := device.AttrGetter().GetInt64("id", true)
  758. if device.Tag != "device" || !ok {
  759. continue
  760. }
  761. user.Device = uint16(deviceID)
  762. devices = append(devices, user)
  763. // TODO take identities here too?
  764. }
  765. // TODO do something with the icdc blob?
  766. return deviceCache{
  767. devices: devices,
  768. dhash: deviceList.AttrGetter().String("dhash"),
  769. }
  770. }
  771. func (cli *Client) getFBIDDevicesInternal(ctx context.Context, jids []types.JID) (*waBinary.Node, error) {
  772. users := make([]waBinary.Node, len(jids))
  773. for i, jid := range jids {
  774. users[i].Tag = "user"
  775. users[i].Attrs = waBinary.Attrs{"jid": jid}
  776. // TODO include dhash for users
  777. }
  778. resp, err := cli.sendIQ(ctx, infoQuery{
  779. Namespace: "fbid:devices",
  780. Type: iqGet,
  781. To: types.ServerJID,
  782. Content: []waBinary.Node{{
  783. Tag: "users",
  784. Content: users,
  785. }},
  786. })
  787. if err != nil {
  788. return nil, fmt.Errorf("failed to send usync query: %w", err)
  789. } else if list, ok := resp.GetOptionalChildByTag("users"); !ok {
  790. return nil, &ElementMissingError{Tag: "users", In: "response to fbid devices query"}
  791. } else {
  792. return &list, err
  793. }
  794. }
  795. func (cli *Client) getFBIDDevices(ctx context.Context, jids []types.JID) ([]types.JID, error) {
  796. var devices []types.JID
  797. for chunk := range slices.Chunk(jids, 15) {
  798. list, err := cli.getFBIDDevicesInternal(ctx, chunk)
  799. if err != nil {
  800. return nil, err
  801. }
  802. for _, user := range list.GetChildren() {
  803. jid, jidOK := user.Attrs["jid"].(types.JID)
  804. if user.Tag != "user" || !jidOK {
  805. continue
  806. }
  807. userDevices := parseFBDeviceList(jid, user.GetChildByTag("devices"))
  808. cli.userDevicesCache[jid] = userDevices
  809. devices = append(devices, userDevices.devices...)
  810. }
  811. }
  812. return devices, nil
  813. }
  814. type UsyncQueryExtras struct {
  815. BotListInfo []types.BotListInfo
  816. }
  817. func (cli *Client) usync(ctx context.Context, jids []types.JID, mode, context string, query []waBinary.Node, extra ...UsyncQueryExtras) (*waBinary.Node, error) {
  818. if cli == nil {
  819. return nil, ErrClientIsNil
  820. }
  821. var extras UsyncQueryExtras
  822. if len(extra) > 1 {
  823. return nil, errors.New("only one extra parameter may be provided to usync()")
  824. } else if len(extra) == 1 {
  825. extras = extra[0]
  826. }
  827. userList := make([]waBinary.Node, len(jids))
  828. for i, jid := range jids {
  829. userList[i].Tag = "user"
  830. jid = jid.ToNonAD()
  831. switch jid.Server {
  832. case types.LegacyUserServer:
  833. userList[i].Content = []waBinary.Node{{
  834. Tag: "contact",
  835. Content: jid.String(),
  836. }}
  837. case types.DefaultUserServer, types.HiddenUserServer:
  838. // NOTE: You can pass in an LID with a JID (<lid jid=...> user node)
  839. // Not sure if you can just put the LID in the jid tag here (works for <devices> queries mainly)
  840. userList[i].Attrs = waBinary.Attrs{"jid": jid}
  841. if jid.IsBot() {
  842. var personaID string
  843. for _, bot := range extras.BotListInfo {
  844. if bot.BotJID.User == jid.User {
  845. personaID = bot.PersonaID
  846. }
  847. }
  848. userList[i].Content = []waBinary.Node{{
  849. Tag: "bot",
  850. Content: []waBinary.Node{{
  851. Tag: "profile",
  852. Attrs: waBinary.Attrs{"persona_id": personaID},
  853. }},
  854. }}
  855. }
  856. default:
  857. return nil, fmt.Errorf("unknown user server '%s'", jid.Server)
  858. }
  859. }
  860. resp, err := cli.sendIQ(ctx, infoQuery{
  861. Namespace: "usync",
  862. Type: "get",
  863. To: types.ServerJID,
  864. Content: []waBinary.Node{{
  865. Tag: "usync",
  866. Attrs: waBinary.Attrs{
  867. "sid": cli.generateRequestID(),
  868. "mode": mode,
  869. "last": "true",
  870. "index": "0",
  871. "context": context,
  872. },
  873. Content: []waBinary.Node{
  874. {Tag: "query", Content: query},
  875. {Tag: "list", Content: userList},
  876. },
  877. }},
  878. })
  879. if err != nil {
  880. return nil, fmt.Errorf("failed to send usync query: %w", err)
  881. } else if list, ok := resp.GetOptionalChildByTag("usync", "list"); !ok {
  882. return nil, &ElementMissingError{Tag: "list", In: "response to usync query"}
  883. } else {
  884. return &list, err
  885. }
  886. }
  887. func (cli *Client) parseBlocklist(node *waBinary.Node) *types.Blocklist {
  888. output := &types.Blocklist{
  889. DHash: node.AttrGetter().String("dhash"),
  890. }
  891. for _, child := range node.GetChildren() {
  892. ag := child.AttrGetter()
  893. blockedJID := ag.JID("jid")
  894. if !ag.OK() {
  895. cli.Log.Debugf("Ignoring contact blocked data with unexpected attributes: %v", ag.Error())
  896. continue
  897. }
  898. output.JIDs = append(output.JIDs, blockedJID)
  899. }
  900. return output
  901. }
  902. // GetBlocklist gets the list of users that this user has blocked.
  903. func (cli *Client) GetBlocklist(ctx context.Context) (*types.Blocklist, error) {
  904. resp, err := cli.sendIQ(ctx, infoQuery{
  905. Namespace: "blocklist",
  906. Type: iqGet,
  907. To: types.ServerJID,
  908. })
  909. if err != nil {
  910. return nil, err
  911. }
  912. list, ok := resp.GetOptionalChildByTag("list")
  913. if !ok {
  914. return nil, &ElementMissingError{Tag: "list", In: "response to blocklist query"}
  915. }
  916. return cli.parseBlocklist(&list), nil
  917. }
  918. // UpdateBlocklist updates the user's block list and returns the updated list.
  919. func (cli *Client) UpdateBlocklist(ctx context.Context, jid types.JID, action events.BlocklistChangeAction) (*types.Blocklist, error) {
  920. resp, err := cli.sendIQ(ctx, infoQuery{
  921. Namespace: "blocklist",
  922. Type: iqSet,
  923. To: types.ServerJID,
  924. Content: []waBinary.Node{{
  925. Tag: "item",
  926. Attrs: waBinary.Attrs{
  927. "jid": jid,
  928. "action": string(action),
  929. },
  930. }},
  931. })
  932. if err != nil {
  933. return nil, err
  934. }
  935. list, ok := resp.GetOptionalChildByTag("list")
  936. if !ok {
  937. return nil, &ElementMissingError{Tag: "list", In: "response to blocklist update"}
  938. }
  939. return cli.parseBlocklist(&list), err
  940. }