// Copyright (c) 2022 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" "crypto/sha256" "encoding/base64" "encoding/binary" "encoding/hex" "errors" "fmt" "slices" "sort" "strconv" "strings" "time" "github.com/rs/zerolog" "go.mau.fi/libsignal/groups" "go.mau.fi/libsignal/keys/prekey" "go.mau.fi/libsignal/protocol" "go.mau.fi/libsignal/session" "go.mau.fi/libsignal/signalerror" "go.mau.fi/util/random" "google.golang.org/protobuf/proto" waBinary "git.bobomao.top/joey/testwh/binary" "git.bobomao.top/joey/testwh/proto/waAICommon" "git.bobomao.top/joey/testwh/proto/waCommon" "git.bobomao.top/joey/testwh/proto/waE2E" "git.bobomao.top/joey/testwh/types" "git.bobomao.top/joey/testwh/types/events" ) const WebMessageIDPrefix = "3EB0" // GenerateMessageID generates a random string that can be used as a message ID on WhatsApp. // // msgID := cli.GenerateMessageID() // cli.SendMessage(context.Background(), targetJID, &waE2E.Message{...}, whatsmeow.SendRequestExtra{ID: msgID}) func (cli *Client) GenerateMessageID() types.MessageID { if cli != nil && cli.MessengerConfig != nil { return types.MessageID(strconv.FormatInt(GenerateFacebookMessageID(), 10)) } data := make([]byte, 8, 8+20+16) binary.BigEndian.PutUint64(data, uint64(time.Now().Unix())) ownID := cli.getOwnID() if !ownID.IsEmpty() { data = append(data, []byte(ownID.User)...) data = append(data, []byte("@c.us")...) } data = append(data, random.Bytes(16)...) hash := sha256.Sum256(data) return WebMessageIDPrefix + strings.ToUpper(hex.EncodeToString(hash[:9])) } func GenerateFacebookMessageID() int64 { const randomMask = (1 << 22) - 1 return (time.Now().UnixMilli() << 22) | (int64(binary.BigEndian.Uint32(random.Bytes(4))) & randomMask) } // GenerateMessageID generates a random string that can be used as a message ID on WhatsApp. // // msgID := whatsmeow.GenerateMessageID() // cli.SendMessage(context.Background(), targetJID, &waE2E.Message{...}, whatsmeow.SendRequestExtra{ID: msgID}) // // Deprecated: WhatsApp web has switched to using a hash of the current timestamp, user id and random bytes. Use Client.GenerateMessageID instead. func GenerateMessageID() types.MessageID { return WebMessageIDPrefix + strings.ToUpper(hex.EncodeToString(random.Bytes(8))) } type MessageDebugTimings struct { LIDFetch time.Duration Queue time.Duration Marshal time.Duration GetParticipants time.Duration GetDevices time.Duration GroupEncrypt time.Duration PeerEncrypt time.Duration Send time.Duration Resp time.Duration Retry time.Duration } func (mdt MessageDebugTimings) MarshalZerologObject(evt *zerolog.Event) { if mdt.LIDFetch != 0 { evt.Dur("lid_fetch", mdt.LIDFetch) } evt.Dur("queue", mdt.Queue) evt.Dur("marshal", mdt.Marshal) if mdt.GetParticipants != 0 { evt.Dur("get_participants", mdt.GetParticipants) } evt.Dur("get_devices", mdt.GetDevices) if mdt.GroupEncrypt != 0 { evt.Dur("group_encrypt", mdt.GroupEncrypt) } evt.Dur("peer_encrypt", mdt.PeerEncrypt) evt.Dur("send", mdt.Send) evt.Dur("resp", mdt.Resp) if mdt.Retry != 0 { evt.Dur("retry", mdt.Retry) } } type SendResponse struct { // The message timestamp returned by the server Timestamp time.Time // The ID of the sent message ID types.MessageID // The server-specified ID of the sent message. Only present for newsletter messages. ServerID types.MessageServerID // Message handling duration, used for debugging DebugTimings MessageDebugTimings // The identity the message was sent with (LID or PN) // This is currently not reliable in all cases. Sender types.JID } // SendRequestExtra contains the optional parameters for SendMessage. // // By default, optional parameters don't have to be provided at all, e.g. // // cli.SendMessage(ctx, to, message) // // When providing optional parameters, add a single instance of this struct as the last parameter: // // cli.SendMessage(ctx, to, message, whatsmeow.SendRequestExtra{...}) // // Trying to add multiple extra parameters will return an error. type SendRequestExtra struct { // The message ID to use when sending. If this is not provided, a random message ID will be generated ID types.MessageID // JID of the bot to be invoked (optional) InlineBotJID types.JID // Should the message be sent as a peer message (protocol messages to your own devices, e.g. app state key requests) Peer bool // A timeout for the send request. Unlike timeouts using the context parameter, this only applies // to the actual response waiting and not preparing/encrypting the message. // Defaults to 75 seconds. The timeout can be disabled by using a negative value. Timeout time.Duration // When sending media to newsletters, the Handle field returned by the file upload. MediaHandle string Meta *types.MsgMetaInfo // use this only if you know what you are doing AdditionalNodes *[]waBinary.Node } // SendMessage sends the given message. // // This method will wait for the server to acknowledge the message before returning. // The return value is the timestamp of the message from the server. // // Optional parameters like the message ID can be specified with the SendRequestExtra struct. // Only one extra parameter is allowed, put all necessary parameters in the same struct. // // The message itself can contain anything you want (within the protobuf schema). // e.g. for a simple text message, use the Conversation field: // // cli.SendMessage(context.Background(), targetJID, &waE2E.Message{ // Conversation: proto.String("Hello, World!"), // }) // // Things like replies, mentioning users and the "forwarded" flag are stored in ContextInfo, // which can be put in ExtendedTextMessage and any of the media message types. // // For uploading and sending media/attachments, see the Upload method. // // For other message types, you'll have to figure it out yourself. Looking at the protobuf schema // in binary/proto/def.proto may be useful to find out all the allowed fields. Printing the RawMessage // field in incoming message events to figure out what it contains is also a good way to learn how to // send the same kind of message. func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waE2E.Message, extra ...SendRequestExtra) (resp SendResponse, err error) { if cli == nil { err = ErrClientIsNil return } var req SendRequestExtra if len(extra) > 1 { err = errors.New("only one extra parameter may be provided to SendMessage") return } else if len(extra) == 1 { req = extra[0] } if to.Device > 0 && !req.Peer { err = ErrRecipientADJID return } ownID := cli.getOwnID() if ownID.IsEmpty() { err = ErrNotLoggedIn return } if req.Timeout == 0 { req.Timeout = defaultRequestTimeout } if len(req.ID) == 0 { req.ID = cli.GenerateMessageID() } if to.Server == types.NewsletterServer { // TODO somehow deduplicate this with the code in sendNewsletter? if message.EditedMessage != nil { req.ID = types.MessageID(message.GetEditedMessage().GetMessage().GetProtocolMessage().GetKey().GetID()) } else if message.ProtocolMessage != nil && message.ProtocolMessage.GetType() == waE2E.ProtocolMessage_REVOKE { req.ID = types.MessageID(message.GetProtocolMessage().GetKey().GetID()) } } resp.ID = req.ID isInlineBotMode := false if !req.InlineBotJID.IsEmpty() { if !req.InlineBotJID.IsBot() { err = ErrInvalidInlineBotID return } isInlineBotMode = true } isBotMode := isInlineBotMode || to.IsBot() needsMessageSecret := isBotMode || cli.shouldIncludeReportingToken(message) var extraParams nodeExtraParams if needsMessageSecret { if message.MessageContextInfo == nil { message.MessageContextInfo = &waE2E.MessageContextInfo{} } if message.MessageContextInfo.MessageSecret == nil { message.MessageContextInfo.MessageSecret = random.Bytes(32) } } if isBotMode { if message.MessageContextInfo.BotMetadata == nil { message.MessageContextInfo.BotMetadata = &waAICommon.BotMetadata{ PersonaID: proto.String("867051314767696$760019659443059"), } } if isInlineBotMode { // inline mode specific code messageSecret := message.GetMessageContextInfo().GetMessageSecret() message = &waE2E.Message{ BotInvokeMessage: &waE2E.FutureProofMessage{ Message: &waE2E.Message{ ExtendedTextMessage: message.ExtendedTextMessage, MessageContextInfo: &waE2E.MessageContextInfo{ BotMetadata: message.MessageContextInfo.BotMetadata, }, }, }, MessageContextInfo: message.MessageContextInfo, } botMessage := &waE2E.Message{ BotInvokeMessage: message.BotInvokeMessage, MessageContextInfo: &waE2E.MessageContextInfo{ BotMetadata: message.MessageContextInfo.BotMetadata, BotMessageSecret: applyBotMessageHKDF(messageSecret), }, } messagePlaintext, _, marshalErr := marshalMessage(req.InlineBotJID, botMessage) if marshalErr != nil { err = marshalErr return } var participantNodes []waBinary.Node participantNodes, _, err = cli.encryptMessageForDevices(ctx, []types.JID{req.InlineBotJID}, resp.ID, messagePlaintext, nil, waBinary.Attrs{}) if err != nil { return } extraParams.botNode = &waBinary.Node{ Tag: "bot", Attrs: nil, Content: participantNodes, } } } var groupParticipants []types.JID if to.Server == types.GroupServer || to.Server == types.BroadcastServer { start := time.Now() if to.Server == types.GroupServer { var cachedData *groupMetaCache cachedData, err = cli.getCachedGroupData(ctx, to) if err != nil { err = fmt.Errorf("failed to get group members: %w", err) return } groupParticipants = cachedData.Members // TODO this is fairly hacky, is there a proper way to determine which identity the message is sent with? if cachedData.AddressingMode == types.AddressingModeLID { ownID = cli.getOwnLID() extraParams.addressingMode = types.AddressingModeLID } else if cachedData.CommunityAnnouncementGroup && req.Meta != nil { ownID = cli.getOwnLID() // Why is this set to PN? extraParams.addressingMode = types.AddressingModePN } } else { groupParticipants, err = cli.getBroadcastListParticipants(ctx, to) if err != nil { err = fmt.Errorf("failed to get broadcast list members: %w", err) return } } resp.DebugTimings.GetParticipants = time.Since(start) } else if to.Server == types.HiddenUserServer { ownID = cli.getOwnLID() } else if to.Server == types.DefaultUserServer && cli.Store.LIDMigrationTimestamp > 0 { start := time.Now() var toLID types.JID toLID, err = cli.Store.LIDs.GetLIDForPN(ctx, to) if err != nil { err = fmt.Errorf("failed to get LID for PN %s: %w", to, err) return } else if toLID.IsEmpty() { var info map[types.JID]types.UserInfo info, err = cli.GetUserInfo(ctx, []types.JID{to}) if err != nil { err = fmt.Errorf("failed to get user info for %s to fill LID cache: %w", to, err) return } else if toLID = info[to].LID; toLID.IsEmpty() { err = fmt.Errorf("no LID found for %s from server", to) return } } resp.DebugTimings.LIDFetch = time.Since(start) cli.Log.Debugf("Replacing SendMessage destination with LID as migration timestamp is set %s -> %s", to, toLID) to = toLID ownID = cli.getOwnLID() } if req.Meta != nil { extraParams.metaNode = &waBinary.Node{ Tag: "meta", Attrs: waBinary.Attrs{}, } if req.Meta.DeprecatedLIDSession != nil { extraParams.metaNode.Attrs["deprecated_lid_session"] = *req.Meta.DeprecatedLIDSession } if req.Meta.ThreadMessageID != "" { extraParams.metaNode.Attrs["thread_msg_id"] = req.Meta.ThreadMessageID extraParams.metaNode.Attrs["thread_msg_sender_jid"] = req.Meta.ThreadMessageSenderJID } } if req.AdditionalNodes != nil { extraParams.additionalNodes = req.AdditionalNodes } resp.Sender = ownID start := time.Now() // Sending multiple messages at a time can cause weird issues and makes it harder to retry safely // This is also required for the session prefetching that makes group sends faster // (everything will explode if you send a message to the same user twice in parallel) cli.messageSendLock.Lock() resp.DebugTimings.Queue = time.Since(start) defer cli.messageSendLock.Unlock() respChan := cli.waitResponse(req.ID) // Peer message retries aren't implemented yet if !req.Peer { cli.addRecentMessage(to, req.ID, message, nil) } if message.GetMessageContextInfo().GetMessageSecret() != nil { err = cli.Store.MsgSecrets.PutMessageSecret(ctx, to, ownID, req.ID, message.GetMessageContextInfo().GetMessageSecret()) if err != nil { cli.Log.Warnf("Failed to store message secret key for outgoing message %s: %v", req.ID, err) } else { cli.Log.Debugf("Stored message secret key for outgoing message %s", req.ID) } } var phash string var data []byte switch to.Server { case types.GroupServer, types.BroadcastServer: phash, data, err = cli.sendGroup(ctx, ownID, to, groupParticipants, req.ID, message, &resp.DebugTimings, extraParams) case types.DefaultUserServer, types.BotServer, types.HiddenUserServer: if req.Peer { data, err = cli.sendPeerMessage(ctx, to, req.ID, message, &resp.DebugTimings) } else { phash, data, err = cli.sendDM(ctx, ownID, to, req.ID, message, &resp.DebugTimings, extraParams) } case types.NewsletterServer: data, err = cli.sendNewsletter(ctx, to, req.ID, message, req.MediaHandle, &resp.DebugTimings) default: err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server) } start = time.Now() if err != nil { cli.cancelResponse(req.ID, respChan) return } var respNode *waBinary.Node var timeoutChan <-chan time.Time if req.Timeout > 0 { timeoutChan = time.After(req.Timeout) } else { timeoutChan = make(<-chan time.Time) } select { case respNode = <-respChan: case <-timeoutChan: cli.cancelResponse(req.ID, respChan) err = ErrMessageTimedOut return case <-ctx.Done(): cli.cancelResponse(req.ID, respChan) err = ctx.Err() return } resp.DebugTimings.Resp = time.Since(start) if isDisconnectNode(respNode) { start = time.Now() respNode, err = cli.retryFrame(ctx, "message send", req.ID, data, respNode, 0) resp.DebugTimings.Retry = time.Since(start) if err != nil { return } } ag := respNode.AttrGetter() resp.ServerID = types.MessageServerID(ag.OptionalInt("server_id")) resp.Timestamp = ag.UnixTime("t") if errorCode := ag.Int("error"); errorCode != 0 { err = fmt.Errorf("%w %d", ErrServerReturnedError, errorCode) } expectedPHash := ag.OptionalString("phash") if len(expectedPHash) > 0 && phash != expectedPHash { cli.Log.Warnf("Server returned different participant list hash (%s != %s) when sending to %s. Some devices may not have received the message.", phash, expectedPHash, to) switch to.Server { case types.GroupServer: // TODO also invalidate device list caches cli.groupCacheLock.Lock() delete(cli.groupCache, to) cli.groupCacheLock.Unlock() case types.BroadcastServer: // TODO do something case types.DefaultUserServer, types.HiddenUserServer, types.BotServer, types.HostedServer, types.HostedLIDServer: cli.userDevicesCacheLock.Lock() delete(cli.userDevicesCache, to) cli.userDevicesCacheLock.Unlock() } } return } // RevokeMessage deletes the given message from everyone in the chat. // // This method will wait for the server to acknowledge the revocation message before returning. // The return value is the timestamp of the message from the server. // // Deprecated: This method is deprecated in favor of BuildRevoke func (cli *Client) RevokeMessage(ctx context.Context, chat types.JID, id types.MessageID) (SendResponse, error) { return cli.SendMessage(ctx, chat, cli.BuildRevoke(chat, types.EmptyJID, id)) } // BuildMessageKey builds a MessageKey object, which is used to refer to previous messages // for things such as replies, revocations and reactions. func (cli *Client) BuildMessageKey(chat, sender types.JID, id types.MessageID) *waCommon.MessageKey { key := &waCommon.MessageKey{ FromMe: proto.Bool(true), ID: proto.String(id), RemoteJID: proto.String(chat.String()), } if !sender.IsEmpty() && sender.User != cli.getOwnID().User && sender.User != cli.getOwnLID().User { key.FromMe = proto.Bool(false) if chat.Server != types.DefaultUserServer && chat.Server != types.HiddenUserServer && chat.Server != types.MessengerServer { key.Participant = proto.String(sender.ToNonAD().String()) } } return key } // BuildRevoke builds a message revocation message using the given variables. // The built message can be sent normally using Client.SendMessage. // // To revoke your own messages, pass your JID or an empty JID as the second parameter (sender). // // resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, types.EmptyJID, originalMessageID) // // To revoke someone else's messages when you are group admin, pass the message sender's JID as the second parameter. // // resp, err := cli.SendMessage(context.Background(), chat, cli.BuildRevoke(chat, senderJID, originalMessageID) func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waE2E.Message { return &waE2E.Message{ ProtocolMessage: &waE2E.ProtocolMessage{ Type: waE2E.ProtocolMessage_REVOKE.Enum(), Key: cli.BuildMessageKey(chat, sender, id), }, } } // BuildReaction builds a message reaction message using the given variables. // The built message can be sent normally using Client.SendMessage. // // resp, err := cli.SendMessage(context.Background(), chat, cli.BuildReaction(chat, senderJID, targetMessageID, "🐈️") // // Note that for newsletter messages, you need to use NewsletterSendReaction instead of BuildReaction + SendMessage. func (cli *Client) BuildReaction(chat, sender types.JID, id types.MessageID, reaction string) *waE2E.Message { return &waE2E.Message{ ReactionMessage: &waE2E.ReactionMessage{ Key: cli.BuildMessageKey(chat, sender, id), Text: proto.String(reaction), SenderTimestampMS: proto.Int64(time.Now().UnixMilli()), }, } } // BuildUnavailableMessageRequest builds a message to request the user's primary device to send // the copy of a message that this client was unable to decrypt. // // The built message can be sent using Client.SendMessage, but you must pass whatsmeow.SendRequestExtra{Peer: true} as the last parameter. // The full response will come as a ProtocolMessage with type `PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE`. // The response events will also be dispatched as normal *events.Message's with UnavailableRequestID set to the request message ID. func (cli *Client) BuildUnavailableMessageRequest(chat, sender types.JID, id string) *waE2E.Message { return &waE2E.Message{ ProtocolMessage: &waE2E.ProtocolMessage{ Type: waE2E.ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE.Enum(), PeerDataOperationRequestMessage: &waE2E.PeerDataOperationRequestMessage{ PeerDataOperationRequestType: waE2E.PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND.Enum(), PlaceholderMessageResendRequest: []*waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest{{ MessageKey: cli.BuildMessageKey(chat, sender, id), }}, }, }, } } // BuildHistorySyncRequest builds a message to request additional history from the user's primary device. // // The built message can be sent using Client.SendMessage, but you must pass whatsmeow.SendRequestExtra{Peer: true} as the last parameter. // The response will come as an *events.HistorySync with type `ON_DEMAND`. // // The response will contain to `count` messages immediately before the given message. // The recommended number of messages to request at a time is 50. func (cli *Client) BuildHistorySyncRequest(lastKnownMessageInfo *types.MessageInfo, count int) *waE2E.Message { return &waE2E.Message{ ProtocolMessage: &waE2E.ProtocolMessage{ Type: waE2E.ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE.Enum(), PeerDataOperationRequestMessage: &waE2E.PeerDataOperationRequestMessage{ PeerDataOperationRequestType: waE2E.PeerDataOperationRequestType_HISTORY_SYNC_ON_DEMAND.Enum(), HistorySyncOnDemandRequest: &waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest{ ChatJID: proto.String(lastKnownMessageInfo.Chat.String()), OldestMsgID: proto.String(lastKnownMessageInfo.ID), OldestMsgFromMe: proto.Bool(lastKnownMessageInfo.IsFromMe), OnDemandMsgCount: proto.Int32(int32(count)), OldestMsgTimestampMS: proto.Int64(lastKnownMessageInfo.Timestamp.UnixMilli()), }, }, }, } } // EditWindow specifies how long a message can be edited for after it was sent. const EditWindow = 20 * time.Minute // BuildEdit builds a message edit message using the given variables. // The built message can be sent normally using Client.SendMessage. // // resp, err := cli.SendMessage(context.Background(), chat, cli.BuildEdit(chat, originalMessageID, &waE2E.Message{ // Conversation: proto.String("edited message"), // }) func (cli *Client) BuildEdit(chat types.JID, id types.MessageID, newContent *waE2E.Message) *waE2E.Message { return &waE2E.Message{ EditedMessage: &waE2E.FutureProofMessage{ Message: &waE2E.Message{ ProtocolMessage: &waE2E.ProtocolMessage{ Key: &waCommon.MessageKey{ FromMe: proto.Bool(true), ID: proto.String(id), RemoteJID: proto.String(chat.String()), }, Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), EditedMessage: newContent, TimestampMS: proto.Int64(time.Now().UnixMilli()), }, }, }, } } const ( DisappearingTimerOff = time.Duration(0) DisappearingTimer24Hours = 24 * time.Hour DisappearingTimer7Days = 7 * 24 * time.Hour DisappearingTimer90Days = 90 * 24 * time.Hour ) // ParseDisappearingTimerString parses common human-readable disappearing message timer strings into Duration values. // If the string doesn't look like one of the allowed values (0, 24h, 7d, 90d), the second return value is false. func ParseDisappearingTimerString(val string) (time.Duration, bool) { switch strings.ReplaceAll(strings.ToLower(val), " ", "") { case "0d", "0h", "0s", "0", "off": return DisappearingTimerOff, true case "1day", "day", "1d", "1", "24h", "24", "86400s", "86400": return DisappearingTimer24Hours, true case "1week", "week", "7d", "7", "168h", "168", "604800s", "604800": return DisappearingTimer7Days, true case "3months", "3m", "3mo", "90d", "90", "2160h", "2160", "7776000s", "7776000": return DisappearingTimer90Days, true default: return 0, false } } // SetDisappearingTimer sets the disappearing timer in a chat. Both private chats and groups are supported, but they're // set with different methods. // // Note that while this function allows passing non-standard durations, official WhatsApp apps will ignore those, // and in groups the server will just reject the change. You can use the DisappearingTimer constants for convenience. // // In groups, the server will echo the change as a notification, so it'll show up as a *events.GroupInfo update. func (cli *Client) SetDisappearingTimer(ctx context.Context, chat types.JID, timer time.Duration, settingTS time.Time) (err error) { switch chat.Server { case types.DefaultUserServer, types.HiddenUserServer: if settingTS.IsZero() { settingTS = time.Now() } _, err = cli.SendMessage(ctx, chat, &waE2E.Message{ ProtocolMessage: &waE2E.ProtocolMessage{ Type: waE2E.ProtocolMessage_EPHEMERAL_SETTING.Enum(), EphemeralExpiration: proto.Uint32(uint32(timer.Seconds())), EphemeralSettingTimestamp: proto.Int64(settingTS.Unix()), }, }) case types.GroupServer: if timer == 0 { _, err = cli.sendGroupIQ(ctx, iqSet, chat, waBinary.Node{Tag: "not_ephemeral"}) } else { _, err = cli.sendGroupIQ(ctx, iqSet, chat, waBinary.Node{ Tag: "ephemeral", Attrs: waBinary.Attrs{ "expiration": strconv.Itoa(int(timer.Seconds())), }, }) if errors.Is(err, ErrIQBadRequest) { err = wrapIQError(ErrInvalidDisappearingTimer, err) } } default: err = fmt.Errorf("can't set disappearing time in a %s chat", chat.Server) } return } func participantListHashV2(participants []types.JID) string { participantsStrings := make([]string, len(participants)) for i, part := range participants { participantsStrings[i] = part.ADString() } sort.Strings(participantsStrings) hash := sha256.Sum256([]byte(strings.Join(participantsStrings, ""))) return fmt.Sprintf("2:%s", base64.RawStdEncoding.EncodeToString(hash[:6])) } func (cli *Client) sendNewsletter( ctx context.Context, to types.JID, id types.MessageID, message *waE2E.Message, mediaID string, timings *MessageDebugTimings, ) ([]byte, error) { attrs := waBinary.Attrs{ "to": to, "id": id, "type": getTypeFromMessage(message), } if mediaID != "" { attrs["media_id"] = mediaID } if message.EditedMessage != nil { attrs["edit"] = string(types.EditAttributeAdminEdit) message = message.GetEditedMessage().GetMessage().GetProtocolMessage().GetEditedMessage() } else if message.ProtocolMessage != nil && message.ProtocolMessage.GetType() == waE2E.ProtocolMessage_REVOKE { attrs["edit"] = string(types.EditAttributeAdminRevoke) message = nil } start := time.Now() plaintext, _, err := marshalMessage(to, message) timings.Marshal = time.Since(start) if err != nil { return nil, err } plaintextNode := waBinary.Node{ Tag: "plaintext", Content: plaintext, Attrs: waBinary.Attrs{}, } if message != nil { if mediaType := getMediaTypeFromMessage(message); mediaType != "" { plaintextNode.Attrs["mediatype"] = mediaType } } node := waBinary.Node{ Tag: "message", Attrs: attrs, Content: []waBinary.Node{plaintextNode}, } start = time.Now() data, err := cli.sendNodeAndGetData(ctx, node) timings.Send = time.Since(start) if err != nil { return nil, fmt.Errorf("failed to send message node: %w", err) } return data, nil } type nodeExtraParams struct { botNode *waBinary.Node metaNode *waBinary.Node additionalNodes *[]waBinary.Node addressingMode types.AddressingMode } func (cli *Client) sendGroup( ctx context.Context, ownID, to types.JID, participants []types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings, extraParams nodeExtraParams, ) (string, []byte, error) { start := time.Now() plaintext, _, err := marshalMessage(to, message) timings.Marshal = time.Since(start) if err != nil { return "", nil, err } start = time.Now() builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer) senderKeyName := protocol.NewSenderKeyName(to.String(), cli.getOwnLID().SignalAddress()) signalSKDMessage, err := builder.Create(ctx, senderKeyName) if err != nil { return "", nil, fmt.Errorf("failed to create sender key distribution message to send %s to %s: %w", id, to, err) } skdMessage := &waE2E.Message{ SenderKeyDistributionMessage: &waE2E.SenderKeyDistributionMessage{ GroupID: proto.String(to.String()), AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(), }, } skdPlaintext, err := proto.Marshal(skdMessage) if err != nil { return "", nil, fmt.Errorf("failed to marshal sender key distribution message to send %s to %s: %w", id, to, err) } cipher := groups.NewGroupCipher(builder, senderKeyName, cli.Store) encrypted, err := cipher.Encrypt(ctx, padMessage(plaintext)) if err != nil { return "", nil, fmt.Errorf("failed to encrypt group message to send %s to %s: %w", id, to, err) } ciphertext := encrypted.SignedSerialize() timings.GroupEncrypt = time.Since(start) node, allDevices, err := cli.prepareMessageNode( ctx, to, id, message, participants, skdPlaintext, nil, timings, extraParams, ) if err != nil { return "", nil, err } phash := participantListHashV2(allDevices) node.Attrs["phash"] = phash skMsg := waBinary.Node{ Tag: "enc", Content: ciphertext, Attrs: waBinary.Attrs{"v": "2", "type": "skmsg"}, } if mediaType := getMediaTypeFromMessage(message); mediaType != "" { skMsg.Attrs["mediatype"] = mediaType } node.Content = append(node.GetChildren(), skMsg) if cli.shouldIncludeReportingToken(message) && message.GetMessageContextInfo().GetMessageSecret() != nil { node.Content = append(node.GetChildren(), cli.getMessageReportingToken(plaintext, message, ownID, to, id)) } start = time.Now() data, err := cli.sendNodeAndGetData(ctx, *node) timings.Send = time.Since(start) if err != nil { return "", nil, fmt.Errorf("failed to send message node: %w", err) } return phash, data, nil } func (cli *Client) sendPeerMessage( ctx context.Context, to types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings, ) ([]byte, error) { node, err := cli.preparePeerMessageNode(ctx, to, id, message, timings) if err != nil { return nil, err } start := time.Now() data, err := cli.sendNodeAndGetData(ctx, *node) timings.Send = time.Since(start) if err != nil { return nil, fmt.Errorf("failed to send message node: %w", err) } return data, nil } func (cli *Client) sendDM( ctx context.Context, ownID, to types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings, extraParams nodeExtraParams, ) (string, []byte, error) { start := time.Now() messagePlaintext, deviceSentMessagePlaintext, err := marshalMessage(to, message) timings.Marshal = time.Since(start) if err != nil { return "", nil, err } node, allDevices, err := cli.prepareMessageNode( ctx, to, id, message, []types.JID{to, ownID.ToNonAD()}, messagePlaintext, deviceSentMessagePlaintext, timings, extraParams, ) if err != nil { return "", nil, err } phash := participantListHashV2(allDevices) if cli.shouldIncludeReportingToken(message) && message.GetMessageContextInfo().GetMessageSecret() != nil { node.Content = append(node.GetChildren(), cli.getMessageReportingToken(messagePlaintext, message, ownID, to, id)) } if tcToken, err := cli.Store.PrivacyTokens.GetPrivacyToken(ctx, to); err != nil { cli.Log.Warnf("Failed to get privacy token for %s: %v", to, err) } else if tcToken != nil { node.Content = append(node.GetChildren(), waBinary.Node{ Tag: "tctoken", Content: tcToken.Token, }) } start = time.Now() data, err := cli.sendNodeAndGetData(ctx, *node) timings.Send = time.Since(start) if err != nil { return "", nil, fmt.Errorf("failed to send message node: %w", err) } return phash, data, nil } func getTypeFromMessage(msg *waE2E.Message) string { switch { case msg.ViewOnceMessage != nil: return getTypeFromMessage(msg.ViewOnceMessage.Message) case msg.ViewOnceMessageV2 != nil: return getTypeFromMessage(msg.ViewOnceMessageV2.Message) case msg.ViewOnceMessageV2Extension != nil: return getTypeFromMessage(msg.ViewOnceMessageV2Extension.Message) case msg.LottieStickerMessage != nil: return getTypeFromMessage(msg.LottieStickerMessage.Message) case msg.EphemeralMessage != nil: return getTypeFromMessage(msg.EphemeralMessage.Message) case msg.DocumentWithCaptionMessage != nil: return getTypeFromMessage(msg.DocumentWithCaptionMessage.Message) case msg.ReactionMessage != nil, msg.EncReactionMessage != nil: return "reaction" case msg.PollCreationMessage != nil, msg.PollUpdateMessage != nil: return "poll" case getMediaTypeFromMessage(msg) != "": return "media" case msg.Conversation != nil, msg.ExtendedTextMessage != nil, msg.ProtocolMessage != nil: return "text" default: return "text" } } func getMediaTypeFromMessage(msg *waE2E.Message) string { switch { case msg.ViewOnceMessage != nil: return getMediaTypeFromMessage(msg.ViewOnceMessage.Message) case msg.ViewOnceMessageV2 != nil: return getMediaTypeFromMessage(msg.ViewOnceMessageV2.Message) case msg.ViewOnceMessageV2Extension != nil: return getMediaTypeFromMessage(msg.ViewOnceMessageV2Extension.Message) case msg.LottieStickerMessage != nil: return getMediaTypeFromMessage(msg.LottieStickerMessage.Message) case msg.EphemeralMessage != nil: return getMediaTypeFromMessage(msg.EphemeralMessage.Message) case msg.DocumentWithCaptionMessage != nil: return getMediaTypeFromMessage(msg.DocumentWithCaptionMessage.Message) case msg.ExtendedTextMessage != nil && msg.ExtendedTextMessage.Title != nil: return "url" case msg.ImageMessage != nil: return "image" case msg.StickerMessage != nil: return "sticker" case msg.DocumentMessage != nil: return "document" case msg.AudioMessage != nil: if msg.AudioMessage.GetPTT() { return "ptt" } else { return "audio" } case msg.VideoMessage != nil: if msg.VideoMessage.GetGifPlayback() { return "gif" } else { return "video" } case msg.ContactMessage != nil: return "vcard" case msg.ContactsArrayMessage != nil: return "contact_array" case msg.ListMessage != nil: return "list" case msg.ListResponseMessage != nil: return "list_response" case msg.ButtonsResponseMessage != nil: return "buttons_response" case msg.OrderMessage != nil: return "order" case msg.ProductMessage != nil: return "product" case msg.InteractiveResponseMessage != nil: return "native_flow_response" default: return "" } } func getButtonTypeFromMessage(msg *waE2E.Message) string { switch { case msg.ViewOnceMessage != nil: return getButtonTypeFromMessage(msg.ViewOnceMessage.Message) case msg.ViewOnceMessageV2 != nil: return getButtonTypeFromMessage(msg.ViewOnceMessageV2.Message) case msg.EphemeralMessage != nil: return getButtonTypeFromMessage(msg.EphemeralMessage.Message) case msg.ButtonsMessage != nil: return "buttons" case msg.ButtonsResponseMessage != nil: return "buttons_response" case msg.ListMessage != nil: return "list" case msg.ListResponseMessage != nil: return "list_response" case msg.InteractiveResponseMessage != nil: return "interactive_response" default: return "" } } func getButtonAttributes(msg *waE2E.Message) waBinary.Attrs { switch { case msg.ViewOnceMessage != nil: return getButtonAttributes(msg.ViewOnceMessage.Message) case msg.ViewOnceMessageV2 != nil: return getButtonAttributes(msg.ViewOnceMessageV2.Message) case msg.EphemeralMessage != nil: return getButtonAttributes(msg.EphemeralMessage.Message) case msg.TemplateMessage != nil: return waBinary.Attrs{} case msg.ListMessage != nil: return waBinary.Attrs{ "v": "2", "type": strings.ToLower(waE2E.ListMessage_ListType_name[int32(msg.ListMessage.GetListType())]), } default: return waBinary.Attrs{} } } const RemoveReactionText = "" func getEditAttribute(msg *waE2E.Message) types.EditAttribute { switch { case msg.EditedMessage != nil && msg.EditedMessage.Message != nil: return getEditAttribute(msg.EditedMessage.Message) case msg.ProtocolMessage != nil && msg.ProtocolMessage.GetKey() != nil: switch msg.ProtocolMessage.GetType() { case waE2E.ProtocolMessage_REVOKE: if msg.ProtocolMessage.GetKey().GetFromMe() { return types.EditAttributeSenderRevoke } else { return types.EditAttributeAdminRevoke } case waE2E.ProtocolMessage_MESSAGE_EDIT: if msg.ProtocolMessage.EditedMessage != nil { return types.EditAttributeMessageEdit } } case msg.ReactionMessage != nil && msg.ReactionMessage.GetText() == RemoveReactionText: return types.EditAttributeSenderRevoke case msg.KeepInChatMessage != nil && msg.KeepInChatMessage.GetKey().GetFromMe() && msg.KeepInChatMessage.GetKeepType() == waE2E.KeepType_UNDO_KEEP_FOR_ALL: return types.EditAttributeSenderRevoke } return types.EditAttributeEmpty } func (cli *Client) preparePeerMessageNode( ctx context.Context, to types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings, ) (*waBinary.Node, error) { attrs := waBinary.Attrs{ "id": id, "type": "text", "category": "peer", "to": to, } if message.GetProtocolMessage().GetType() == waE2E.ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST { attrs["push_priority"] = "high" } start := time.Now() plaintext, err := proto.Marshal(message) timings.Marshal = time.Since(start) if err != nil { err = fmt.Errorf("failed to marshal message: %w", err) return nil, err } encryptionIdentity := to if to.Server == types.DefaultUserServer { encryptionIdentity, err = cli.Store.LIDs.GetLIDForPN(ctx, to) if err != nil { return nil, fmt.Errorf("failed to get LID for PN %s: %w", to, err) } } start = time.Now() encrypted, isPreKey, err := cli.encryptMessageForDevice(ctx, plaintext, encryptionIdentity, nil, nil, nil) timings.PeerEncrypt = time.Since(start) if err != nil { return nil, fmt.Errorf("failed to encrypt peer message for %s: %v", to, err) } content := []waBinary.Node{{ Tag: "meta", Attrs: waBinary.Attrs{ "appdata": "default", }, }, *encrypted} if isPreKey && cli.MessengerConfig == nil { content = append(content, cli.makeDeviceIdentityNode()) } return &waBinary.Node{ Tag: "message", Attrs: attrs, Content: content, }, nil } func (cli *Client) getMessageContent( baseNode waBinary.Node, message *waE2E.Message, msgAttrs waBinary.Attrs, includeIdentity bool, extraParams nodeExtraParams, ) []waBinary.Node { content := []waBinary.Node{baseNode} if includeIdentity { content = append(content, cli.makeDeviceIdentityNode()) } if msgAttrs["type"] == "poll" { pollType := "creation" if message.PollUpdateMessage != nil { pollType = "vote" } content = append(content, waBinary.Node{ Tag: "meta", Attrs: waBinary.Attrs{ "polltype": pollType, }, }) } if extraParams.botNode != nil { content = append(content, *extraParams.botNode) } if extraParams.metaNode != nil { content = append(content, *extraParams.metaNode) } if extraParams.additionalNodes != nil { content = append(content, *extraParams.additionalNodes...) } if buttonType := getButtonTypeFromMessage(message); buttonType != "" { content = append(content, waBinary.Node{ Tag: "biz", Content: []waBinary.Node{{ Tag: buttonType, Attrs: getButtonAttributes(message), }}, }) } return content } func (cli *Client) prepareMessageNode( ctx context.Context, to types.JID, id types.MessageID, message *waE2E.Message, participants []types.JID, plaintext, dsmPlaintext []byte, timings *MessageDebugTimings, extraParams nodeExtraParams, ) (*waBinary.Node, []types.JID, error) { start := time.Now() allDevices, err := cli.GetUserDevices(ctx, participants) timings.GetDevices = time.Since(start) if err != nil { return nil, nil, fmt.Errorf("failed to get device list: %w", err) } if to.Server == types.GroupServer { allDevices = slices.DeleteFunc(allDevices, func(jid types.JID) bool { return jid.Server == types.HostedServer || jid.Server == types.HostedLIDServer }) } msgType := getTypeFromMessage(message) encAttrs := waBinary.Attrs{} // Only include encMediaType for 1:1 messages (groups don't have a device-sent message plaintext) if encMediaType := getMediaTypeFromMessage(message); dsmPlaintext != nil && encMediaType != "" { encAttrs["mediatype"] = encMediaType } attrs := waBinary.Attrs{ "id": id, "type": msgType, "to": to, } // TODO this is a very hacky hack for announcement group messages, why is it pn anyway? if extraParams.addressingMode != "" { attrs["addressing_mode"] = string(extraParams.addressingMode) } if editAttr := getEditAttribute(message); editAttr != "" { attrs["edit"] = string(editAttr) encAttrs["decrypt-fail"] = string(events.DecryptFailHide) } if msgType == "reaction" || message.GetPollUpdateMessage() != nil { encAttrs["decrypt-fail"] = string(events.DecryptFailHide) } start = time.Now() participantNodes, includeIdentity, err := cli.encryptMessageForDevices( ctx, allDevices, id, plaintext, dsmPlaintext, encAttrs, ) timings.PeerEncrypt = time.Since(start) if err != nil { return nil, nil, err } participantNode := waBinary.Node{ Tag: "participants", Content: participantNodes, } return &waBinary.Node{ Tag: "message", Attrs: attrs, Content: cli.getMessageContent( participantNode, message, attrs, includeIdentity, extraParams, ), }, allDevices, nil } func marshalMessage(to types.JID, message *waE2E.Message) (plaintext, dsmPlaintext []byte, err error) { if message == nil && to.Server == types.NewsletterServer { return } plaintext, err = proto.Marshal(message) if err != nil { err = fmt.Errorf("failed to marshal message: %w", err) return } if to.Server != types.GroupServer && to.Server != types.NewsletterServer { dsmPlaintext, err = proto.Marshal(&waE2E.Message{ DeviceSentMessage: &waE2E.DeviceSentMessage{ DestinationJID: proto.String(to.String()), Message: message, }, MessageContextInfo: message.MessageContextInfo, }) if err != nil { err = fmt.Errorf("failed to marshal message (for own devices): %w", err) return } } return } func (cli *Client) makeDeviceIdentityNode() waBinary.Node { deviceIdentity, err := proto.Marshal(cli.Store.Account) if err != nil { panic(fmt.Errorf("failed to marshal device identity: %w", err)) } return waBinary.Node{ Tag: "device-identity", Content: deviceIdentity, } } func (cli *Client) encryptMessageForDevices( ctx context.Context, allDevices []types.JID, id string, msgPlaintext, dsmPlaintext []byte, encAttrs waBinary.Attrs, ) ([]waBinary.Node, bool, error) { ownJID := cli.getOwnID() ownLID := cli.getOwnLID() includeIdentity := false participantNodes := make([]waBinary.Node, 0, len(allDevices)) var pnDevices []types.JID for _, jid := range allDevices { if jid.Server == types.DefaultUserServer { pnDevices = append(pnDevices, jid) } } lidMappings, err := cli.Store.LIDs.GetManyLIDsForPNs(ctx, pnDevices) if err != nil { return nil, false, fmt.Errorf("failed to fetch LID mappings: %w", err) } encryptionIdentities := make(map[types.JID]types.JID, len(allDevices)) sessionAddressToJID := make(map[string]types.JID, len(allDevices)) sessionAddresses := make([]string, 0, len(allDevices)) for _, jid := range allDevices { encryptionIdentity := jid if jid.Server == types.DefaultUserServer { // TODO query LID from server for missing entries if lidForPN, ok := lidMappings[jid]; ok && !lidForPN.IsEmpty() { cli.migrateSessionStore(ctx, jid, lidForPN) encryptionIdentity = lidForPN } } encryptionIdentities[jid] = encryptionIdentity addr := encryptionIdentity.SignalAddress().String() sessionAddresses = append(sessionAddresses, addr) sessionAddressToJID[addr] = jid } existingSessions, ctx, err := cli.Store.WithCachedSessions(ctx, sessionAddresses) if err != nil { return nil, false, fmt.Errorf("failed to prefetch sessions: %w", err) } var retryDevices []types.JID for addr, exists := range existingSessions { if !exists { retryDevices = append(retryDevices, sessionAddressToJID[addr]) } } bundles := cli.fetchPreKeysNoError(ctx, retryDevices) for _, jid := range allDevices { plaintext := msgPlaintext if (jid.User == ownJID.User || jid.User == ownLID.User) && dsmPlaintext != nil { if jid == ownJID || jid == ownLID { continue } plaintext = dsmPlaintext } encrypted, isPreKey, err := cli.encryptMessageForDeviceAndWrap( ctx, plaintext, jid, encryptionIdentities[jid], bundles[jid], encAttrs, existingSessions, ) if err != nil { // TODO return these errors if it's a fatal one (like context cancellation or database) cli.Log.Warnf("Failed to encrypt %s for %s: %v", id, jid, err) if ctx.Err() != nil { return nil, false, err } continue } participantNodes = append(participantNodes, *encrypted) if isPreKey { includeIdentity = true } } err = cli.Store.PutCachedSessions(ctx) if err != nil { return nil, false, fmt.Errorf("failed to save cached sessions: %w", err) } return participantNodes, includeIdentity, nil } func (cli *Client) encryptMessageForDeviceAndWrap( ctx context.Context, plaintext []byte, wireIdentity, encryptionIdentity types.JID, bundle *prekey.Bundle, encAttrs waBinary.Attrs, existingSessions map[string]bool, ) (*waBinary.Node, bool, error) { node, includeDeviceIdentity, err := cli.encryptMessageForDevice( ctx, plaintext, encryptionIdentity, bundle, encAttrs, existingSessions, ) if err != nil { return nil, false, err } return &waBinary.Node{ Tag: "to", Attrs: waBinary.Attrs{"jid": wireIdentity}, Content: []waBinary.Node{*node}, }, includeDeviceIdentity, nil } func copyAttrs(from, to waBinary.Attrs) { for k, v := range from { to[k] = v } } func (cli *Client) encryptMessageForDevice( ctx context.Context, plaintext []byte, to types.JID, bundle *prekey.Bundle, extraAttrs waBinary.Attrs, existingSessions map[string]bool, ) (*waBinary.Node, bool, error) { builder := session.NewBuilderFromSignal(cli.Store, to.SignalAddress(), pbSerializer) if bundle != nil { cli.Log.Debugf("Processing prekey bundle for %s", to) err := builder.ProcessBundle(ctx, bundle) if cli.AutoTrustIdentity && errors.Is(err, signalerror.ErrUntrustedIdentity) { cli.Log.Warnf("Got %v error while trying to process prekey bundle for %s, clearing stored identity and retrying", err, to) err = cli.clearUntrustedIdentity(ctx, to) if err != nil { return nil, false, fmt.Errorf("failed to clear untrusted identity: %w", err) } err = builder.ProcessBundle(ctx, bundle) } if err != nil { return nil, false, fmt.Errorf("failed to process prekey bundle: %w", err) } } else { sessionExists, checked := existingSessions[to.SignalAddress().String()] if !checked { var err error sessionExists, err = cli.Store.ContainsSession(ctx, to.SignalAddress()) if err != nil { return nil, false, err } } if !sessionExists { return nil, false, fmt.Errorf("%w with %s", ErrNoSession, to.SignalAddress().String()) } } cipher := session.NewCipher(builder, to.SignalAddress()) ciphertext, err := cipher.Encrypt(ctx, padMessage(plaintext)) if err != nil { return nil, false, fmt.Errorf("cipher encryption failed: %w", err) } encAttrs := waBinary.Attrs{ "v": "2", "type": "msg", } if ciphertext.Type() == protocol.PREKEY_TYPE { encAttrs["type"] = "pkmsg" } copyAttrs(extraAttrs, encAttrs) includeDeviceIdentity := encAttrs["type"] == "pkmsg" && cli.MessengerConfig == nil return &waBinary.Node{ Tag: "enc", Attrs: encAttrs, Content: ciphertext.Serialize(), }, includeDeviceIdentity, nil }