| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965 |
- // Copyright (c) 2021 Tulir Asokan
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this
- // file, You can obtain one at http://mozilla.org/MPL/2.0/.
- package whatsmeow
- import (
- "context"
- "encoding/base64"
- "errors"
- "fmt"
- "slices"
- "strings"
- "google.golang.org/protobuf/proto"
- waBinary "go.mau.fi/whatsmeow/binary"
- "go.mau.fi/whatsmeow/proto/waHistorySync"
- "go.mau.fi/whatsmeow/proto/waVnameCert"
- "go.mau.fi/whatsmeow/store"
- "go.mau.fi/whatsmeow/types"
- "go.mau.fi/whatsmeow/types/events"
- )
- const (
- BusinessMessageLinkPrefix = "https://wa.me/message/"
- ContactQRLinkPrefix = "https://wa.me/qr/"
- BusinessMessageLinkDirectPrefix = "https://api.whatsapp.com/message/"
- ContactQRLinkDirectPrefix = "https://api.whatsapp.com/qr/"
- NewsletterLinkPrefix = "https://whatsapp.com/channel/"
- )
- // ResolveBusinessMessageLink resolves a business message short link and returns the target JID, business name and
- // text to prefill in the input field (if any).
- //
- // The links look like https://wa.me/message/<code> or https://api.whatsapp.com/message/<code>. You can either provide
- // the full link, or just the <code> part.
- func (cli *Client) ResolveBusinessMessageLink(ctx context.Context, code string) (*types.BusinessMessageLinkTarget, error) {
- code = strings.TrimPrefix(code, BusinessMessageLinkPrefix)
- code = strings.TrimPrefix(code, BusinessMessageLinkDirectPrefix)
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "w:qr",
- Type: iqGet,
- // WhatsApp android doesn't seem to have a "to" field for this one at all, not sure why but it works
- Content: []waBinary.Node{{
- Tag: "qr",
- Attrs: waBinary.Attrs{
- "code": code,
- },
- }},
- })
- if errors.Is(err, ErrIQNotFound) {
- return nil, wrapIQError(ErrBusinessMessageLinkNotFound, err)
- } else if err != nil {
- return nil, err
- }
- qrChild, ok := resp.GetOptionalChildByTag("qr")
- if !ok {
- return nil, &ElementMissingError{Tag: "qr", In: "response to business message link query"}
- }
- var target types.BusinessMessageLinkTarget
- ag := qrChild.AttrGetter()
- target.JID = ag.JID("jid")
- target.PushName = ag.String("notify")
- messageChild, ok := qrChild.GetOptionalChildByTag("message")
- if ok {
- messageBytes, _ := messageChild.Content.([]byte)
- target.Message = string(messageBytes)
- }
- businessChild, ok := qrChild.GetOptionalChildByTag("business")
- if ok {
- bag := businessChild.AttrGetter()
- target.IsSigned = bag.OptionalBool("is_signed")
- target.VerifiedName = bag.OptionalString("verified_name")
- target.VerifiedLevel = bag.OptionalString("verified_level")
- }
- return &target, ag.Error()
- }
- // ResolveContactQRLink resolves a link from a contact share QR code and returns the target JID and push name.
- //
- // The links look like https://wa.me/qr/<code> or https://api.whatsapp.com/qr/<code>. You can either provide
- // the full link, or just the <code> part.
- func (cli *Client) ResolveContactQRLink(ctx context.Context, code string) (*types.ContactQRLinkTarget, error) {
- code = strings.TrimPrefix(code, ContactQRLinkPrefix)
- code = strings.TrimPrefix(code, ContactQRLinkDirectPrefix)
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "w:qr",
- Type: iqGet,
- Content: []waBinary.Node{{
- Tag: "qr",
- Attrs: waBinary.Attrs{
- "code": code,
- },
- }},
- })
- if errors.Is(err, ErrIQNotFound) {
- return nil, wrapIQError(ErrContactQRLinkNotFound, err)
- } else if err != nil {
- return nil, err
- }
- qrChild, ok := resp.GetOptionalChildByTag("qr")
- if !ok {
- return nil, &ElementMissingError{Tag: "qr", In: "response to contact link query"}
- }
- var target types.ContactQRLinkTarget
- ag := qrChild.AttrGetter()
- target.JID = ag.JID("jid")
- target.PushName = ag.OptionalString("notify")
- target.Type = ag.String("type")
- return &target, ag.Error()
- }
- // GetContactQRLink gets your own contact share QR link that can be resolved using ResolveContactQRLink
- // (or scanned with the official apps when encoded as a QR code).
- //
- // If the revoke parameter is set to true, it will ask the server to revoke the previous link and generate a new one.
- func (cli *Client) GetContactQRLink(ctx context.Context, revoke bool) (string, error) {
- action := "get"
- if revoke {
- action = "revoke"
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "w:qr",
- Type: iqSet,
- Content: []waBinary.Node{{
- Tag: "qr",
- Attrs: waBinary.Attrs{
- "type": "contact",
- "action": action,
- },
- }},
- })
- if err != nil {
- return "", err
- }
- qrChild, ok := resp.GetOptionalChildByTag("qr")
- if !ok {
- return "", &ElementMissingError{Tag: "qr", In: "response to own contact link fetch"}
- }
- ag := qrChild.AttrGetter()
- return ag.String("code"), ag.Error()
- }
- // SetStatusMessage updates the current user's status text, which is shown in the "About" section in the user profile.
- //
- // This is different from the ephemeral status broadcast messages. Use SendMessage to types.StatusBroadcastJID to send
- // such messages.
- func (cli *Client) SetStatusMessage(ctx context.Context, msg string) error {
- _, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "status",
- Type: iqSet,
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "status",
- Content: msg,
- }},
- })
- return err
- }
- // IsOnWhatsApp checks if the given phone numbers are registered on WhatsApp.
- // The phone numbers should be in international format, including the `+` prefix.
- func (cli *Client) IsOnWhatsApp(ctx context.Context, phones []string) ([]types.IsOnWhatsAppResponse, error) {
- jids := make([]types.JID, len(phones))
- for i := range jids {
- jids[i] = types.NewJID(phones[i], types.LegacyUserServer)
- }
- list, err := cli.usync(ctx, jids, "query", "interactive", []waBinary.Node{
- {Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
- {Tag: "contact"},
- })
- if err != nil {
- return nil, err
- }
- output := make([]types.IsOnWhatsAppResponse, 0, len(jids))
- querySuffix := "@" + types.LegacyUserServer
- for _, child := range list.GetChildren() {
- jid, jidOK := child.Attrs["jid"].(types.JID)
- if child.Tag != "user" || !jidOK {
- continue
- }
- var info types.IsOnWhatsAppResponse
- info.JID = jid
- info.VerifiedName, err = parseVerifiedName(child.GetChildByTag("business"))
- if err != nil {
- cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
- }
- contactNode := child.GetChildByTag("contact")
- info.IsIn = contactNode.AttrGetter().String("type") == "in"
- contactQuery, _ := contactNode.Content.([]byte)
- info.Query = strings.TrimSuffix(string(contactQuery), querySuffix)
- output = append(output, info)
- }
- return output, nil
- }
- // GetUserInfo gets basic user info (avatar, status, verified business name, device list).
- func (cli *Client) GetUserInfo(ctx context.Context, jids []types.JID) (map[types.JID]types.UserInfo, error) {
- list, err := cli.usync(ctx, jids, "full", "background", []waBinary.Node{
- {Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}},
- {Tag: "status"},
- {Tag: "picture"},
- {Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
- {Tag: "lid"},
- })
- if err != nil {
- return nil, err
- }
- respData := make(map[types.JID]types.UserInfo, len(jids))
- mappings := make([]store.LIDMapping, 0, len(jids))
- for _, child := range list.GetChildren() {
- jid, jidOK := child.Attrs["jid"].(types.JID)
- if child.Tag != "user" || !jidOK {
- continue
- }
- var info types.UserInfo
- verifiedName, err := parseVerifiedName(child.GetChildByTag("business"))
- if err != nil {
- cli.Log.Warnf("Failed to parse %s's verified name details: %v", jid, err)
- }
- status, _ := child.GetChildByTag("status").Content.([]byte)
- info.Status = string(status)
- info.PictureID, _ = child.GetChildByTag("picture").Attrs["id"].(string)
- info.Devices = parseDeviceList(jid, child.GetChildByTag("devices"))
- lidTag := child.GetChildByTag("lid")
- info.LID = lidTag.AttrGetter().OptionalJIDOrEmpty("val")
- if !info.LID.IsEmpty() {
- mappings = append(mappings, store.LIDMapping{PN: jid, LID: info.LID})
- }
- if verifiedName != nil {
- cli.updateBusinessName(ctx, jid, info.LID, nil, verifiedName.Details.GetVerifiedName())
- }
- respData[jid] = info
- }
- err = cli.Store.LIDs.PutManyLIDMappings(ctx, mappings)
- if err != nil {
- // not worth returning on the error, instead just post a log
- cli.Log.Errorf("Failed to place LID mappings from USync call")
- }
- return respData, nil
- }
- func (cli *Client) GetBotListV2(ctx context.Context) ([]types.BotListInfo, error) {
- resp, err := cli.sendIQ(ctx, infoQuery{
- To: types.ServerJID,
- Namespace: "bot",
- Type: iqGet,
- Content: []waBinary.Node{
- {Tag: "bot", Attrs: waBinary.Attrs{"v": "2"}},
- },
- })
- if err != nil {
- return nil, err
- }
- botNode, ok := resp.GetOptionalChildByTag("bot")
- if !ok {
- return nil, &ElementMissingError{Tag: "bot", In: "response to bot list query"}
- }
- var list []types.BotListInfo
- for _, section := range botNode.GetChildrenByTag("section") {
- if section.AttrGetter().String("type") == "all" {
- for _, bot := range section.GetChildrenByTag("bot") {
- ag := bot.AttrGetter()
- list = append(list, types.BotListInfo{
- PersonaID: ag.String("persona_id"),
- BotJID: ag.JID("jid"),
- })
- }
- }
- }
- return list, nil
- }
- func (cli *Client) GetBotProfiles(ctx context.Context, botInfo []types.BotListInfo) ([]types.BotProfileInfo, error) {
- jids := make([]types.JID, len(botInfo))
- for i, bot := range botInfo {
- jids[i] = bot.BotJID
- }
- list, err := cli.usync(ctx, jids, "query", "interactive", []waBinary.Node{
- {Tag: "bot", Content: []waBinary.Node{{Tag: "profile", Attrs: waBinary.Attrs{"v": "1"}}}},
- }, UsyncQueryExtras{
- BotListInfo: botInfo,
- })
- if err != nil {
- return nil, err
- }
- var profiles []types.BotProfileInfo
- for _, user := range list.GetChildren() {
- jid := user.AttrGetter().JID("jid")
- bot := user.GetChildByTag("bot")
- profile := bot.GetChildByTag("profile")
- name := string(profile.GetChildByTag("name").Content.([]byte))
- attributes := string(profile.GetChildByTag("attributes").Content.([]byte))
- description := string(profile.GetChildByTag("description").Content.([]byte))
- category := string(profile.GetChildByTag("category").Content.([]byte))
- _, isDefault := profile.GetOptionalChildByTag("default")
- personaID := profile.AttrGetter().String("persona_id")
- commandsNode := profile.GetChildByTag("commands")
- commandDescription := string(commandsNode.GetChildByTag("description").Content.([]byte))
- var commands []types.BotProfileCommand
- for _, commandNode := range commandsNode.GetChildrenByTag("command") {
- commands = append(commands, types.BotProfileCommand{
- Name: string(commandNode.GetChildByTag("name").Content.([]byte)),
- Description: string(commandNode.GetChildByTag("description").Content.([]byte)),
- })
- }
- promptsNode := profile.GetChildByTag("prompts")
- var prompts []string
- for _, promptNode := range promptsNode.GetChildrenByTag("prompt") {
- prompts = append(
- prompts,
- fmt.Sprintf(
- "%s %s",
- string(promptNode.GetChildByTag("emoji").Content.([]byte)),
- string(promptNode.GetChildByTag("text").Content.([]byte)),
- ),
- )
- }
- profiles = append(profiles, types.BotProfileInfo{
- JID: jid,
- Name: name,
- Attributes: attributes,
- Description: description,
- Category: category,
- IsDefault: isDefault,
- Prompts: prompts,
- PersonaID: personaID,
- Commands: commands,
- CommandsDescription: commandDescription,
- })
- }
- return profiles, nil
- }
- func (cli *Client) parseBusinessProfile(node *waBinary.Node) (*types.BusinessProfile, error) {
- profileNode := node.GetChildByTag("profile")
- jid, ok := profileNode.AttrGetter().GetJID("jid", true)
- if !ok {
- return nil, errors.New("missing jid in business profile")
- }
- address, _ := profileNode.GetChildByTag("address").Content.([]byte)
- email, _ := profileNode.GetChildByTag("email").Content.([]byte)
- businessHour := profileNode.GetChildByTag("business_hours")
- businessHourTimezone := businessHour.AttrGetter().String("timezone")
- businessHoursConfigs := businessHour.GetChildren()
- businessHours := make([]types.BusinessHoursConfig, 0)
- for _, config := range businessHoursConfigs {
- if config.Tag != "business_hours_config" {
- continue
- }
- dow := config.AttrGetter().String("day_of_week")
- mode := config.AttrGetter().String("mode")
- openTime := config.AttrGetter().String("open_time")
- closeTime := config.AttrGetter().String("close_time")
- businessHours = append(businessHours, types.BusinessHoursConfig{
- DayOfWeek: dow,
- Mode: mode,
- OpenTime: openTime,
- CloseTime: closeTime,
- })
- }
- categoriesNode := profileNode.GetChildByTag("categories")
- categories := make([]types.Category, 0)
- for _, category := range categoriesNode.GetChildren() {
- if category.Tag != "category" {
- continue
- }
- id := category.AttrGetter().String("id")
- name, _ := category.Content.([]byte)
- categories = append(categories, types.Category{
- ID: id,
- Name: string(name),
- })
- }
- profileOptionsNode := profileNode.GetChildByTag("profile_options")
- profileOptions := make(map[string]string)
- for _, option := range profileOptionsNode.GetChildren() {
- optValueBytes, _ := option.Content.([]byte)
- profileOptions[option.Tag] = string(optValueBytes)
- // TODO parse bot_fields
- }
- return &types.BusinessProfile{
- JID: jid,
- Email: string(email),
- Address: string(address),
- Categories: categories,
- ProfileOptions: profileOptions,
- BusinessHoursTimeZone: businessHourTimezone,
- BusinessHours: businessHours,
- }, nil
- }
- // GetBusinessProfile gets the profile info of a WhatsApp business account
- func (cli *Client) GetBusinessProfile(ctx context.Context, jid types.JID) (*types.BusinessProfile, error) {
- resp, err := cli.sendIQ(ctx, infoQuery{
- Type: iqGet,
- To: types.ServerJID,
- Namespace: "w:biz",
- Content: []waBinary.Node{{
- Tag: "business_profile",
- Attrs: waBinary.Attrs{
- "v": "244",
- },
- Content: []waBinary.Node{{
- Tag: "profile",
- Attrs: waBinary.Attrs{
- "jid": jid,
- },
- }},
- }},
- })
- if err != nil {
- return nil, err
- }
- node, ok := resp.GetOptionalChildByTag("business_profile")
- if !ok {
- return nil, &ElementMissingError{Tag: "business_profile", In: "response to business profile query"}
- }
- return cli.parseBusinessProfile(&node)
- }
- func (cli *Client) GetUserDevicesContext(ctx context.Context, jids []types.JID) ([]types.JID, error) {
- return cli.GetUserDevices(ctx, jids)
- }
- // GetUserDevices gets the list of devices that the given user has. The input should be a list of
- // regular JIDs, and the output will be a list of AD JIDs. The local device will not be included in
- // the output even if the user's JID is included in the input. All other devices will be included.
- func (cli *Client) GetUserDevices(ctx context.Context, jids []types.JID) ([]types.JID, error) {
- if cli == nil {
- return nil, ErrClientIsNil
- }
- cli.userDevicesCacheLock.Lock()
- defer cli.userDevicesCacheLock.Unlock()
- var devices, jidsToSync, fbJIDsToSync []types.JID
- for _, jid := range jids {
- cached, ok := cli.userDevicesCache[jid]
- if ok && len(cached.devices) > 0 {
- devices = append(devices, cached.devices...)
- } else if jid.Server == types.MessengerServer {
- fbJIDsToSync = append(fbJIDsToSync, jid)
- } else if jid.IsBot() {
- // Bot JIDs do not have devices, the usync query is empty
- devices = append(devices, jid)
- } else {
- jidsToSync = append(jidsToSync, jid)
- }
- }
- if len(jidsToSync) > 0 {
- list, err := cli.usync(ctx, jidsToSync, "query", "message", []waBinary.Node{
- {Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
- })
- if err != nil {
- return nil, err
- }
- for _, user := range list.GetChildren() {
- jid, jidOK := user.Attrs["jid"].(types.JID)
- if user.Tag != "user" || !jidOK {
- continue
- }
- userDevices := parseDeviceList(jid, user.GetChildByTag("devices"))
- cli.userDevicesCache[jid] = deviceCache{devices: userDevices, dhash: participantListHashV2(userDevices)}
- devices = append(devices, userDevices...)
- }
- }
- if len(fbJIDsToSync) > 0 {
- userDevices, err := cli.getFBIDDevices(ctx, fbJIDsToSync)
- if err != nil {
- return nil, err
- }
- devices = append(devices, userDevices...)
- }
- return devices, nil
- }
- type GetProfilePictureParams struct {
- Preview bool
- ExistingID string
- IsCommunity bool
- // This is a common group ID that you share with the target
- CommonGID types.JID
- // use this to query the profile photo of a group you don't have joined, but you have an invite code for
- InviteCode string
- // Persona ID when getting profile of Meta AI bots
- PersonaID string
- }
- // GetProfilePictureInfo gets the URL where you can download a WhatsApp user's profile picture or group's photo.
- //
- // Optionally, you can pass the last known profile picture ID.
- // If the profile picture hasn't changed, this will return nil with no error.
- //
- // To get a community photo, you should pass `IsCommunity: true`, as otherwise you may get a 401 error.
- func (cli *Client) GetProfilePictureInfo(ctx context.Context, jid types.JID, params *GetProfilePictureParams) (*types.ProfilePictureInfo, error) {
- if cli == nil {
- return nil, ErrClientIsNil
- }
- attrs := waBinary.Attrs{
- "query": "url",
- }
- var target, to types.JID
- if params == nil {
- params = &GetProfilePictureParams{}
- }
- if params.Preview {
- attrs["type"] = "preview"
- } else {
- attrs["type"] = "image"
- }
- if params.ExistingID != "" {
- attrs["id"] = params.ExistingID
- }
- if params.InviteCode != "" {
- attrs["invite"] = params.InviteCode
- }
- var expectWrapped bool
- var content []waBinary.Node
- namespace := "w:profile:picture"
- if params.IsCommunity {
- target = types.EmptyJID
- namespace = "w:g2"
- to = jid
- attrs["parent_group_jid"] = jid
- expectWrapped = true
- content = []waBinary.Node{{
- Tag: "pictures",
- Content: []waBinary.Node{{
- Tag: "picture",
- Attrs: attrs,
- }},
- }}
- } else {
- to = types.ServerJID
- target = jid
- if !params.CommonGID.IsEmpty() {
- attrs["common_gid"] = params.CommonGID
- }
- if params.PersonaID != "" {
- attrs["persona_id"] = params.PersonaID
- }
- var pictureContent []waBinary.Node
- if token, _ := cli.Store.PrivacyTokens.GetPrivacyToken(ctx, jid); token != nil {
- pictureContent = []waBinary.Node{{
- Tag: "tctoken",
- Content: token.Token,
- }}
- }
- content = []waBinary.Node{{
- Tag: "picture",
- Attrs: attrs,
- Content: pictureContent,
- }}
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: namespace,
- Type: "get",
- To: to,
- Target: target,
- Content: content,
- })
- if errors.Is(err, ErrIQNotAuthorized) {
- return nil, wrapIQError(ErrProfilePictureUnauthorized, err)
- } else if errors.Is(err, ErrIQNotFound) {
- return nil, wrapIQError(ErrProfilePictureNotSet, err)
- } else if err != nil {
- return nil, err
- }
- if expectWrapped {
- pics, ok := resp.GetOptionalChildByTag("pictures")
- if !ok {
- return nil, &ElementMissingError{Tag: "pictures", In: "response to profile picture query"}
- }
- resp = &pics
- }
- picture, ok := resp.GetOptionalChildByTag("picture")
- if !ok {
- if params.ExistingID != "" {
- return nil, nil
- }
- return nil, &ElementMissingError{Tag: "picture", In: "response to profile picture query"}
- }
- var info types.ProfilePictureInfo
- ag := picture.AttrGetter()
- if ag.OptionalInt("status") == 304 {
- return nil, nil
- } else if ag.OptionalInt("status") == 204 {
- return nil, ErrProfilePictureNotSet
- }
- info.ID = ag.String("id")
- info.URL = ag.String("url")
- info.Type = ag.String("type")
- info.DirectPath = ag.String("direct_path")
- info.Hash, _ = base64.StdEncoding.DecodeString(ag.OptionalString("hash"))
- if !ag.OK() {
- return &info, ag.Error()
- }
- return &info, nil
- }
- func (cli *Client) handleHistoricalPushNames(ctx context.Context, names []*waHistorySync.Pushname) {
- if cli.Store.Contacts == nil {
- return
- }
- cli.Log.Infof("Updating contact store with %d push names from history sync", len(names))
- for _, user := range names {
- if user.GetPushname() == "-" {
- continue
- }
- var changed bool
- if jid, err := types.ParseJID(user.GetID()); err != nil {
- cli.Log.Warnf("Failed to parse user ID '%s' in push name history sync: %v", user.GetID(), err)
- } else if changed, _, err = cli.Store.Contacts.PutPushName(ctx, jid, user.GetPushname()); err != nil {
- cli.Log.Warnf("Failed to store push name of %s from history sync: %v", jid, err)
- } else if changed {
- cli.Log.Debugf("Got push name %s for %s in history sync", user.GetPushname(), jid)
- }
- }
- }
- func (cli *Client) updatePushName(ctx context.Context, user, userAlt types.JID, messageInfo *types.MessageInfo, name string) {
- if cli.Store.Contacts == nil {
- return
- }
- user = user.ToNonAD()
- changed, previousName, err := cli.Store.Contacts.PutPushName(ctx, user, name)
- if err != nil {
- cli.Log.Errorf("Failed to save push name of %s in device store: %v", user, err)
- } else if changed {
- userAlt = userAlt.ToNonAD()
- if userAlt.IsEmpty() {
- userAlt, _ = cli.Store.GetAltJID(ctx, user)
- }
- if !userAlt.IsEmpty() {
- _, _, err = cli.Store.Contacts.PutPushName(ctx, userAlt, name)
- if err != nil {
- cli.Log.Errorf("Failed to save push name of %s in device store: %v", userAlt, err)
- }
- }
- cli.Log.Debugf("Push name of %s changed from %s to %s, dispatching event", user, previousName, name)
- cli.dispatchEvent(&events.PushName{
- JID: user,
- JIDAlt: userAlt,
- Message: messageInfo,
- OldPushName: previousName,
- NewPushName: name,
- })
- }
- }
- func (cli *Client) updateBusinessName(ctx context.Context, user, userAlt types.JID, messageInfo *types.MessageInfo, name string) {
- if cli.Store.Contacts == nil {
- return
- }
- changed, previousName, err := cli.Store.Contacts.PutBusinessName(ctx, user, name)
- if err != nil {
- cli.Log.Errorf("Failed to save business name of %s in device store: %v", user, err)
- } else if changed {
- userAlt = userAlt.ToNonAD()
- if userAlt.IsEmpty() {
- userAlt, _ = cli.Store.GetAltJID(ctx, user)
- }
- if !userAlt.IsEmpty() {
- _, _, err = cli.Store.Contacts.PutBusinessName(ctx, userAlt, name)
- if err != nil {
- cli.Log.Errorf("Failed to save push name of %s in device store: %v", userAlt, err)
- }
- }
- cli.Log.Debugf("Business name of %s changed from %s to %s, dispatching event", user, previousName, name)
- cli.dispatchEvent(&events.BusinessName{
- JID: user,
- Message: messageInfo,
- OldBusinessName: previousName,
- NewBusinessName: name,
- })
- }
- }
- func parseVerifiedName(businessNode waBinary.Node) (*types.VerifiedName, error) {
- if businessNode.Tag != "business" {
- return nil, nil
- }
- verifiedNameNode, ok := businessNode.GetOptionalChildByTag("verified_name")
- if !ok {
- return nil, nil
- }
- return parseVerifiedNameContent(verifiedNameNode)
- }
- func parseVerifiedNameContent(verifiedNameNode waBinary.Node) (*types.VerifiedName, error) {
- rawCert, ok := verifiedNameNode.Content.([]byte)
- if !ok {
- return nil, nil
- }
- var cert waVnameCert.VerifiedNameCertificate
- err := proto.Unmarshal(rawCert, &cert)
- if err != nil {
- return nil, err
- }
- var certDetails waVnameCert.VerifiedNameCertificate_Details
- err = proto.Unmarshal(cert.GetDetails(), &certDetails)
- if err != nil {
- return nil, err
- }
- return &types.VerifiedName{
- Certificate: &cert,
- Details: &certDetails,
- }, nil
- }
- func parseDeviceList(user types.JID, deviceNode waBinary.Node) []types.JID {
- deviceList := deviceNode.GetChildByTag("device-list")
- if deviceNode.Tag != "devices" || deviceList.Tag != "device-list" {
- return nil
- }
- children := deviceList.GetChildren()
- devices := make([]types.JID, 0, len(children))
- for _, device := range children {
- deviceID, ok := device.AttrGetter().GetInt64("id", true)
- isHosted := device.AttrGetter().Bool("is_hosted")
- if device.Tag != "device" || !ok {
- continue
- }
- user.Device = uint16(deviceID)
- if isHosted {
- hostedUser := user
- if user.Server == types.HiddenUserServer {
- hostedUser.Server = types.HostedLIDServer
- } else {
- hostedUser.Server = types.HostedServer
- }
- devices = append(devices, hostedUser)
- } else {
- devices = append(devices, user)
- }
- }
- return devices
- }
- func parseFBDeviceList(user types.JID, deviceList waBinary.Node) deviceCache {
- children := deviceList.GetChildren()
- devices := make([]types.JID, 0, len(children))
- for _, device := range children {
- deviceID, ok := device.AttrGetter().GetInt64("id", true)
- if device.Tag != "device" || !ok {
- continue
- }
- user.Device = uint16(deviceID)
- devices = append(devices, user)
- // TODO take identities here too?
- }
- // TODO do something with the icdc blob?
- return deviceCache{
- devices: devices,
- dhash: deviceList.AttrGetter().String("dhash"),
- }
- }
- func (cli *Client) getFBIDDevicesInternal(ctx context.Context, jids []types.JID) (*waBinary.Node, error) {
- users := make([]waBinary.Node, len(jids))
- for i, jid := range jids {
- users[i].Tag = "user"
- users[i].Attrs = waBinary.Attrs{"jid": jid}
- // TODO include dhash for users
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "fbid:devices",
- Type: iqGet,
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "users",
- Content: users,
- }},
- })
- if err != nil {
- return nil, fmt.Errorf("failed to send usync query: %w", err)
- } else if list, ok := resp.GetOptionalChildByTag("users"); !ok {
- return nil, &ElementMissingError{Tag: "users", In: "response to fbid devices query"}
- } else {
- return &list, err
- }
- }
- func (cli *Client) getFBIDDevices(ctx context.Context, jids []types.JID) ([]types.JID, error) {
- var devices []types.JID
- for chunk := range slices.Chunk(jids, 15) {
- list, err := cli.getFBIDDevicesInternal(ctx, chunk)
- if err != nil {
- return nil, err
- }
- for _, user := range list.GetChildren() {
- jid, jidOK := user.Attrs["jid"].(types.JID)
- if user.Tag != "user" || !jidOK {
- continue
- }
- userDevices := parseFBDeviceList(jid, user.GetChildByTag("devices"))
- cli.userDevicesCache[jid] = userDevices
- devices = append(devices, userDevices.devices...)
- }
- }
- return devices, nil
- }
- type UsyncQueryExtras struct {
- BotListInfo []types.BotListInfo
- }
- func (cli *Client) usync(ctx context.Context, jids []types.JID, mode, context string, query []waBinary.Node, extra ...UsyncQueryExtras) (*waBinary.Node, error) {
- if cli == nil {
- return nil, ErrClientIsNil
- }
- var extras UsyncQueryExtras
- if len(extra) > 1 {
- return nil, errors.New("only one extra parameter may be provided to usync()")
- } else if len(extra) == 1 {
- extras = extra[0]
- }
- userList := make([]waBinary.Node, len(jids))
- for i, jid := range jids {
- userList[i].Tag = "user"
- jid = jid.ToNonAD()
- switch jid.Server {
- case types.LegacyUserServer:
- userList[i].Content = []waBinary.Node{{
- Tag: "contact",
- Content: jid.String(),
- }}
- case types.DefaultUserServer, types.HiddenUserServer:
- // NOTE: You can pass in an LID with a JID (<lid jid=...> user node)
- // Not sure if you can just put the LID in the jid tag here (works for <devices> queries mainly)
- userList[i].Attrs = waBinary.Attrs{"jid": jid}
- if jid.IsBot() {
- var personaID string
- for _, bot := range extras.BotListInfo {
- if bot.BotJID.User == jid.User {
- personaID = bot.PersonaID
- }
- }
- userList[i].Content = []waBinary.Node{{
- Tag: "bot",
- Content: []waBinary.Node{{
- Tag: "profile",
- Attrs: waBinary.Attrs{"persona_id": personaID},
- }},
- }}
- }
- default:
- return nil, fmt.Errorf("unknown user server '%s'", jid.Server)
- }
- }
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "usync",
- Type: "get",
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "usync",
- Attrs: waBinary.Attrs{
- "sid": cli.generateRequestID(),
- "mode": mode,
- "last": "true",
- "index": "0",
- "context": context,
- },
- Content: []waBinary.Node{
- {Tag: "query", Content: query},
- {Tag: "list", Content: userList},
- },
- }},
- })
- if err != nil {
- return nil, fmt.Errorf("failed to send usync query: %w", err)
- } else if list, ok := resp.GetOptionalChildByTag("usync", "list"); !ok {
- return nil, &ElementMissingError{Tag: "list", In: "response to usync query"}
- } else {
- return &list, err
- }
- }
- func (cli *Client) parseBlocklist(node *waBinary.Node) *types.Blocklist {
- output := &types.Blocklist{
- DHash: node.AttrGetter().String("dhash"),
- }
- for _, child := range node.GetChildren() {
- ag := child.AttrGetter()
- blockedJID := ag.JID("jid")
- if !ag.OK() {
- cli.Log.Debugf("Ignoring contact blocked data with unexpected attributes: %v", ag.Error())
- continue
- }
- output.JIDs = append(output.JIDs, blockedJID)
- }
- return output
- }
- // GetBlocklist gets the list of users that this user has blocked.
- func (cli *Client) GetBlocklist(ctx context.Context) (*types.Blocklist, error) {
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "blocklist",
- Type: iqGet,
- To: types.ServerJID,
- })
- if err != nil {
- return nil, err
- }
- list, ok := resp.GetOptionalChildByTag("list")
- if !ok {
- return nil, &ElementMissingError{Tag: "list", In: "response to blocklist query"}
- }
- return cli.parseBlocklist(&list), nil
- }
- // UpdateBlocklist updates the user's block list and returns the updated list.
- func (cli *Client) UpdateBlocklist(ctx context.Context, jid types.JID, action events.BlocklistChangeAction) (*types.Blocklist, error) {
- resp, err := cli.sendIQ(ctx, infoQuery{
- Namespace: "blocklist",
- Type: iqSet,
- To: types.ServerJID,
- Content: []waBinary.Node{{
- Tag: "item",
- Attrs: waBinary.Attrs{
- "jid": jid,
- "action": string(action),
- },
- }},
- })
- if err != nil {
- return nil, err
- }
- list, ok := resp.GetOptionalChildByTag("list")
- if !ok {
- return nil, &ElementMissingError{Tag: "list", In: "response to blocklist update"}
- }
- return cli.parseBlocklist(&list), err
- }
|