| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400 |
- // 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 "go.mau.fi/whatsmeow/binary"
- "go.mau.fi/whatsmeow/proto/waAICommon"
- "go.mau.fi/whatsmeow/proto/waCommon"
- "go.mau.fi/whatsmeow/proto/waE2E"
- "go.mau.fi/whatsmeow/types"
- "go.mau.fi/whatsmeow/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<Duration> 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
- }
|