Skip to main content

whatsapp_rust/send/
actions.rs

1use super::*;
2
3impl Client {
4    /// Delete a message for everyone in the chat (revoke).
5    ///
6    /// This sends a revoke protocol message that removes the message for all participants.
7    /// The message will show as "This message was deleted" for recipients.
8    ///
9    /// # Arguments
10    /// * `to` - The chat JID (DM or group)
11    /// * `message_id` - The ID of the message to delete
12    /// * `revoke_type` - Use `RevokeType::Sender` to delete your own message,
13    ///   or `RevokeType::Admin { original_sender }` to delete another user's message as group admin
14    pub async fn revoke_message(
15        &self,
16        to: impl Into<Jid>,
17        message_id: impl Into<String>,
18        revoke_type: RevokeType,
19    ) -> Result<(), SendError> {
20        self.revoke_message_inner(to.into(), message_id.into(), revoke_type)
21            .await
22    }
23
24    #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.revoke", level = "debug", skip_all, fields(to = %to.observe()), err(Debug)))]
25    async fn revoke_message_inner(
26        &self,
27        to: Jid,
28        message_id: String,
29        revoke_type: RevokeType,
30    ) -> Result<(), SendError> {
31        self.require_pn().map_err(SendError::from_anyhow)?;
32
33        let (from_me, participant, edit_attr) = match &revoke_type {
34            RevokeType::Sender => {
35                // For sender revoke, participant is NOT set (from_me=true identifies it)
36                // This matches whatsmeow's BuildMessageKey behavior
37                (true, None, EditAttribute::SenderRevoke)
38            }
39            RevokeType::Admin { original_sender } => {
40                // Admin revoke requires group context
41                if !to.is_group() {
42                    return Err(SendError::InvalidRequest(
43                        "admin revoke is only valid for group chats".into(),
44                    ));
45                }
46                // The protocolMessageKey.participant should match the original message's key exactly
47                // Do NOT convert LID to PN - pass through unchanged like WhatsApp Web does
48                let participant_str = original_sender.to_non_ad_string();
49                log::debug!(
50                    "Admin revoke: using participant {} for MessageKey",
51                    participant_str
52                );
53                (false, Some(participant_str), EditAttribute::AdminRevoke)
54            }
55        };
56
57        let revoke_message = build_revoke_message(&to, from_me, message_id, participant);
58
59        // The revoke message stanza needs a NEW unique ID, not the message ID being revoked
60        // The message_id being revoked is already in protocolMessage.key.id
61        // Passing None generates a fresh stanza ID
62        //
63        // For admin revokes, force SKDM distribution to get the proper message structure
64        // with phash, <participants>, and <device-identity> that WhatsApp Web uses
65        let force_skdm = matches!(revoke_type, RevokeType::Admin { .. });
66        self.send_message_impl(
67            to,
68            &revoke_message,
69            SendPipelineOptions {
70                force_key_distribution: force_skdm,
71                edit: Some(edit_attr),
72                ..Default::default()
73            },
74        )
75        .await
76        .map_err(SendError::from_anyhow)?;
77        Ok(())
78    }
79
80    /// Keep (or un-keep) a message in a disappearing chat for everyone.
81    ///
82    /// Sends a `keepInChatMessage` add-on (WA Web `WAWebKeepInChatMsgAction`):
83    /// `keep = true` requests `KEEP_FOR_ALL`, `keep = false` requests
84    /// `UNDO_KEEP_FOR_ALL`. `key` is the target (kept) message's key; the keep
85    /// message itself is sent with a fresh id. The send path classifies this as a
86    /// text add-on and maps the undo case to a sender-revoke edit attribute.
87    pub async fn keep_message(
88        &self,
89        chat: impl Into<Jid>,
90        key: wa::MessageKey,
91        keep: bool,
92    ) -> Result<SendResult, SendError> {
93        let chat = chat.into();
94        let message = wacore::proto_helpers::build_keep_in_chat_message(
95            key,
96            keep,
97            wacore::time::now_millis(),
98        );
99        self.send_message(chat, message).await
100    }
101
102    /// Pin a message in a chat for all participants.
103    pub async fn pin_message(
104        &self,
105        chat: impl Into<Jid>,
106        key: wa::MessageKey,
107        duration: PinDuration,
108    ) -> Result<(), SendError> {
109        self.send_pin(
110            chat.into(),
111            key,
112            wa::message::pin_in_chat_message::Type::PinForAll,
113            duration.as_secs(),
114        )
115        .await
116    }
117
118    /// Unpin a previously pinned message.
119    pub async fn unpin_message(
120        &self,
121        chat: impl Into<Jid>,
122        key: wa::MessageKey,
123    ) -> Result<(), SendError> {
124        self.send_pin(
125            chat.into(),
126            key,
127            wa::message::pin_in_chat_message::Type::UnpinForAll,
128            0,
129        )
130        .await
131    }
132
133    #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.pin", level = "debug", skip_all, fields(chat = %chat.observe()), err(Debug)))]
134    async fn send_pin(
135        &self,
136        chat: Jid,
137        key: wa::MessageKey,
138        pin_type: wa::message::pin_in_chat_message::Type,
139        duration_secs: u32,
140    ) -> Result<(), SendError> {
141        let message = wa::Message {
142            pin_in_chat_message: buffa::MessageField::some(wa::message::PinInChatMessage {
143                key: buffa::MessageField::some(key),
144                r#type: Some(pin_type),
145                sender_timestamp_ms: Some(wacore::time::now_millis()),
146            }),
147            message_context_info: buffa::MessageField::some(wa::MessageContextInfo {
148                message_add_on_duration_in_secs: Some(duration_secs),
149                ..Default::default()
150            }),
151            ..Default::default()
152        };
153
154        self.send_message_impl(
155            chat,
156            &message,
157            SendPipelineOptions {
158                edit: Some(EditAttribute::PinInChat),
159                ..Default::default()
160            },
161        )
162        .await
163        .map_err(SendError::from_anyhow)?;
164        Ok(())
165    }
166}