diff options
| author | Mistivia <i@mistivia.com> | 2025-11-02 15:27:18 +0800 |
|---|---|---|
| committer | Mistivia <i@mistivia.com> | 2025-11-02 15:27:18 +0800 |
| commit | e9c24f4af7ed56760f6db7941827d09f6db9020b (patch) | |
| tree | 62128c43b883ce5e3148113350978755779bb5de /teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/user.go | |
| parent | 58d5e7cfda4781d8a57ec52aefd02983835c301a (diff) | |
add matterbridge
Diffstat (limited to 'teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/user.go')
| -rw-r--r-- | teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/user.go | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/user.go b/teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/user.go new file mode 100644 index 0000000..dae8640 --- /dev/null +++ b/teleirc/matterbridge/vendor/go.mau.fi/whatsmeow/user.go @@ -0,0 +1,501 @@ +// 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" + "errors" + "fmt" + "strings" + + "google.golang.org/protobuf/proto" + + waBinary "go.mau.fi/whatsmeow/binary" + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" +) + +const BusinessMessageLinkPrefix = "https://wa.me/message/" +const ContactQRLinkPrefix = "https://wa.me/qr/" +const BusinessMessageLinkDirectPrefix = "https://api.whatsapp.com/message/" +const ContactQRLinkDirectPrefix = "https://api.whatsapp.com/qr/" + +// 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(code string) (*types.BusinessMessageLinkTarget, error) { + code = strings.TrimPrefix(code, BusinessMessageLinkPrefix) + code = strings.TrimPrefix(code, BusinessMessageLinkDirectPrefix) + + resp, err := cli.sendIQ(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(code string) (*types.ContactQRLinkTarget, error) { + code = strings.TrimPrefix(code, ContactQRLinkPrefix) + code = strings.TrimPrefix(code, ContactQRLinkDirectPrefix) + + resp, err := cli.sendIQ(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(revoke bool) (string, error) { + action := "get" + if revoke { + action = "revoke" + } + resp, err := cli.sendIQ(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(msg string) error { + _, err := cli.sendIQ(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(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(context.TODO(), 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(jids []types.JID) (map[types.JID]types.UserInfo, error) { + list, err := cli.usync(context.TODO(), jids, "full", "background", []waBinary.Node{ + {Tag: "business", Content: []waBinary.Node{{Tag: "verified_name"}}}, + {Tag: "status"}, + {Tag: "picture"}, + {Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}}, + }) + if err != nil { + return nil, err + } + respData := make(map[types.JID]types.UserInfo, 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.User, child.GetChildByTag("devices")) + if verifiedName != nil { + cli.updateBusinessName(jid, nil, verifiedName.Details.GetVerifiedName()) + } + respData[jid] = info + } + return respData, nil +} + +// 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(jids []types.JID) ([]types.JID, error) { + return cli.GetUserDevicesContext(context.Background(), jids) +} + +func (cli *Client) GetUserDevicesContext(ctx context.Context, jids []types.JID) ([]types.JID, error) { + cli.userDevicesCacheLock.Lock() + defer cli.userDevicesCacheLock.Unlock() + + var devices, jidsToSync []types.JID + for _, jid := range jids { + cached, ok := cli.userDevicesCache[jid] + if ok && len(cached) > 0 { + devices = append(devices, cached...) + } else { + jidsToSync = append(jidsToSync, jid) + } + } + if len(jidsToSync) == 0 { + return devices, nil + } + + 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, user.GetChildByTag("devices")) + cli.userDevicesCache[jid] = userDevices + devices = append(devices, userDevices...) + } + + return devices, nil +} + +type GetProfilePictureParams struct { + Preview bool + ExistingID string + IsCommunity bool +} + +// 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(jid types.JID, params *GetProfilePictureParams) (*types.ProfilePictureInfo, error) { + attrs := waBinary.Attrs{ + "query": "url", + } + if params == nil { + params = &GetProfilePictureParams{} + } + if params.Preview { + attrs["type"] = "preview" + } else { + attrs["type"] = "image" + } + if params.ExistingID != "" { + attrs["id"] = params.ExistingID + } + var pictureContent []waBinary.Node + namespace := "w:profile:picture" + if params.IsCommunity { + namespace = "w:g2" + pictureContent = []waBinary.Node{{ + Tag: "query_linked", + Attrs: waBinary.Attrs{ + "type": "parent_group", + "jid": jid, + }, + }} + } + resp, err := cli.sendIQ(infoQuery{ + Namespace: namespace, + Type: "get", + To: jid, + Content: []waBinary.Node{{ + Tag: "picture", + Attrs: attrs, + Content: pictureContent, + }}, + }) + 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 + } + 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() + info.ID = ag.String("id") + info.URL = ag.String("url") + info.Type = ag.String("type") + info.DirectPath = ag.String("direct_path") + if !ag.OK() { + return &info, ag.Error() + } + return &info, nil +} + +func (cli *Client) handleHistoricalPushNames(names []*waProto.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(jid, user.GetPushname()); err != nil { + cli.Log.Warnf("Failed to store push name of %s from history sync: %v", err) + } else if changed { + cli.Log.Debugf("Got push name %s for %s in history sync", user.GetPushname(), jid) + } + } +} + +func (cli *Client) updatePushName(user types.JID, messageInfo *types.MessageInfo, name string) { + if cli.Store.Contacts == nil { + return + } + user = user.ToNonAD() + changed, previousName, err := cli.Store.Contacts.PutPushName(user, name) + if err != nil { + cli.Log.Errorf("Failed to save push name of %s in device store: %v", user, err) + } else if changed { + cli.Log.Debugf("Push name of %s changed from %s to %s, dispatching event", user, previousName, name) + cli.dispatchEvent(&events.PushName{ + JID: user, + Message: messageInfo, + OldPushName: previousName, + NewPushName: name, + }) + } +} + +func (cli *Client) updateBusinessName(user types.JID, messageInfo *types.MessageInfo, name string) { + if cli.Store.Contacts == nil { + return + } + changed, previousName, err := cli.Store.Contacts.PutBusinessName(user, name) + if err != nil { + cli.Log.Errorf("Failed to save business name of %s in device store: %v", user, err) + } else if changed { + 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 waProto.VerifiedNameCertificate + err := proto.Unmarshal(rawCert, &cert) + if err != nil { + return nil, err + } + var certDetails waProto.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 string, 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) + if device.Tag != "device" || !ok { + continue + } + devices = append(devices, types.NewADJID(user, 0, byte(deviceID))) + } + return devices +} + +func (cli *Client) usync(ctx context.Context, jids []types.JID, mode, context string, query []waBinary.Node) (*waBinary.Node, error) { + userList := make([]waBinary.Node, len(jids)) + for i, jid := range jids { + userList[i].Tag = "user" + if jid.AD { + jid.AD = false + } + switch jid.Server { + case types.LegacyUserServer: + userList[i].Content = []waBinary.Node{{ + Tag: "contact", + Content: jid.String(), + }} + case types.DefaultUserServer: + userList[i].Attrs = waBinary.Attrs{"jid": jid} + default: + return nil, fmt.Errorf("unknown user server '%s'", jid.Server) + } + } + resp, err := cli.sendIQ(infoQuery{ + Context: ctx, + 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 + } +} |
