Skip to main content

whatsapp_rust/features/
events.rs

1//! Event creation and response (RSVP).
2
3use wacore::event;
4use wacore_binary::{Jid, JidExt};
5use waproto::whatsapp as wa;
6
7pub use waproto::whatsapp::message::event_response_message::EventResponseType;
8
9use crate::client::Client;
10use crate::send::{SendError, SendResult};
11
12/// Parameters for creating an event message. Only `name` is required.
13#[derive(Debug, Clone, Default)]
14pub struct EventCreationParams {
15    pub name: String,
16    pub description: Option<String>,
17    pub start_time: Option<i64>,
18    pub end_time: Option<i64>,
19    pub join_link: Option<String>,
20    pub location: Option<wa::message::LocationMessage>,
21    pub is_scheduled_call: Option<bool>,
22    pub extra_guests_allowed: Option<bool>,
23}
24
25pub struct Events<'a> {
26    client: &'a Client,
27}
28
29impl<'a> Events<'a> {
30    pub(crate) fn new(client: &'a Client) -> Self {
31        Self { client }
32    }
33
34    /// Create an event. Returns the `message_secret` the creator needs to decrypt
35    /// later responses (RSVPs) via [`wacore::event::decrypt_event_response_with_secret`].
36    pub async fn create(
37        &self,
38        to: impl Into<Jid>,
39        params: EventCreationParams,
40    ) -> Result<(SendResult, Vec<u8>), SendError> {
41        let to = &to.into();
42        if params.name.trim().is_empty() {
43            return Err(SendError::InvalidRequest(
44                "event name must not be empty".into(),
45            ));
46        }
47
48        let mut message = wa::Message {
49            event_message: buffa::MessageField::some(build_event_message(params)),
50            ..Default::default()
51        };
52
53        // Events carry a per-message secret (like polls); responders derive their
54        // RSVP encryption key from it. WA Web rejects an event without one
55        // (Events/ValidationError MISSING_MESSAGE_SECRET).
56        let message_secret: Vec<u8> = {
57            use rand::Rng;
58            let mut secret = vec![0u8; 32];
59            rand::rng().fill_bytes(&mut secret);
60            secret
61        };
62        message.message_context_info = buffa::MessageField::some(wa::MessageContextInfo {
63            message_secret: Some(message_secret.clone()),
64            ..Default::default()
65        });
66
67        let result = self.client.send_message(to, message).await?;
68        Ok((result, message_secret))
69    }
70
71    /// RSVP to an event. `message_secret` is the event's secret (from its creation
72    /// message); `event_creator_jid` is who created the event.
73    pub async fn respond(
74        &self,
75        chat_jid: impl Into<Jid>,
76        event_msg_id: &str,
77        event_creator_jid: &Jid,
78        message_secret: &[u8],
79        response: EventResponseType,
80        extra_guest_count: Option<i32>,
81    ) -> Result<SendResult, SendError> {
82        let chat_jid = &chat_jid.into();
83        // The event secret keys the RSVP's HKDF; a wrong length is caller error,
84        // not an internal failure, so reject it before the encrypt call.
85        if message_secret.len() != 32 {
86            return Err(SendError::InvalidRequest(format!(
87                "event message_secret must be 32 bytes, got {}",
88                message_secret.len()
89            )));
90        }
91        let my_jid = self.client.pn().ok_or(SendError::NotLoggedIn)?;
92        let my_base = my_jid.to_non_ad();
93
94        let responder = self
95            .resolve_responder_jid(event_creator_jid, &my_base)
96            .await;
97        let responder_str = responder.to_string();
98        let creator_str = event_creator_jid.to_non_ad_string();
99
100        let response_msg = wa::message::EventResponseMessage {
101            response: Some(response),
102            timestamp_ms: Some(wacore::time::now_millis()),
103            extra_guest_count,
104        };
105
106        let (enc_payload, iv) = event::encrypt_event_response_with_secret(
107            &response_msg,
108            message_secret,
109            event_msg_id,
110            &creator_str,
111            &responder_str,
112        )?;
113
114        let from_me = my_base.is_same_user_as(event_creator_jid);
115        let enc = wa::message::EncEventResponseMessage {
116            event_creation_message_key: buffa::MessageField::some(wa::MessageKey {
117                remote_jid: Some(chat_jid.to_string()),
118                from_me: Some(from_me),
119                id: Some(event_msg_id.to_string()),
120                participant: if chat_jid.is_group() {
121                    Some(event_creator_jid.to_string())
122                } else {
123                    None
124                },
125            }),
126            enc_payload: Some(enc_payload),
127            enc_iv: Some(iv.to_vec()),
128        };
129
130        let message = wa::Message {
131            enc_event_response_message: buffa::MessageField::some(enc),
132            ..Default::default()
133        };
134
135        self.client.send_message(chat_jid, message).await
136    }
137
138    /// The responder (self) JID keys the RSVP's HKDF/AAD, so it must use the event
139    /// creator's namespace: own LID for a LID-addressed event, own PN otherwise,
140    /// falling back to PN when our LID isn't known. Mirrors the poll-vote path.
141    async fn resolve_responder_jid(&self, event_creator_jid: &Jid, own_pn: &Jid) -> Jid {
142        if !event_creator_jid.is_lid() {
143            return own_pn.clone();
144        }
145        match self.client.lid() {
146            Some(lid) => lid.to_non_ad(),
147            None => own_pn.clone(),
148        }
149    }
150}
151
152impl Client {
153    pub fn events(&self) -> Events<'_> {
154        Events::new(self)
155    }
156}
157
158/// Build an `EventMessage` from the public params. Mirrors WA Web's
159/// `GenerateEventCreationMessageProto` field set.
160fn build_event_message(params: EventCreationParams) -> wa::message::EventMessage {
161    wa::message::EventMessage {
162        name: Some(params.name),
163        description: params.description,
164        start_time: params.start_time,
165        end_time: params.end_time,
166        join_link: params.join_link,
167        location: params.location.into(),
168        is_schedule_call: params.is_scheduled_call,
169        extra_guests_allowed: params.extra_guests_allowed,
170        ..Default::default()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::test_utils::create_test_client;
178    use std::sync::Arc;
179
180    #[test]
181    fn build_event_message_maps_fields() {
182        let params = EventCreationParams {
183            name: "Launch".into(),
184            description: Some("desc".into()),
185            start_time: Some(1_700_000_000),
186            end_time: Some(1_700_003_600),
187            join_link: Some("https://call".into()),
188            is_scheduled_call: Some(true),
189            extra_guests_allowed: Some(true),
190            ..Default::default()
191        };
192        let msg = build_event_message(params);
193        assert_eq!(msg.name.as_deref(), Some("Launch"));
194        assert_eq!(msg.description.as_deref(), Some("desc"));
195        assert_eq!(msg.start_time, Some(1_700_000_000));
196        assert_eq!(msg.end_time, Some(1_700_003_600));
197        assert_eq!(msg.join_link.as_deref(), Some("https://call"));
198        assert_eq!(msg.is_schedule_call, Some(true));
199        assert_eq!(msg.extra_guests_allowed, Some(true));
200        assert!(msg.is_canceled.is_none());
201    }
202
203    #[tokio::test]
204    async fn responder_is_pn_when_creator_is_pn() {
205        let client: Arc<Client> = create_test_client().await;
206        let own_pn = Jid::pn("5511999999999");
207        let creator = Jid::pn("5511777777777");
208        let responder = client
209            .events()
210            .resolve_responder_jid(&creator, &own_pn)
211            .await;
212        assert_eq!(responder, own_pn);
213    }
214}