deltachat/webxdc/
integration.rs

1use std::path::Path;
2
3use crate::chat::{send_msg, ChatId};
4use crate::config::Config;
5use crate::contact::ContactId;
6use crate::context::Context;
7use crate::message::{Message, MsgId, Viewtype};
8use crate::param::Param;
9use crate::webxdc::{maps_integration, StatusUpdateItem, StatusUpdateSerial};
10use anyhow::Result;
11
12impl Context {
13    /// Sets Webxdc file as integration.
14    /// `file` is the .xdc to use as Webxdc integration.
15    pub async fn set_webxdc_integration(&self, file: &str) -> Result<()> {
16        let chat_id = ChatId::create_for_contact(self, ContactId::SELF).await?;
17        let mut msg = Message::new(Viewtype::Webxdc);
18        msg.set_file_and_deduplicate(self, Path::new(&file), None, None)?;
19        msg.hidden = true;
20        msg.param.set_int(Param::WebxdcIntegration, 1);
21        msg.param.set_int(Param::GuaranteeE2ee, 1); // needed to pass `internet_access` requirements
22        send_msg(self, chat_id, &mut msg).await?;
23        Ok(())
24    }
25
26    /// Returns Webxdc instance used for optional integrations.
27    /// UI can open the Webxdc as usual.
28    /// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
29    /// `integrate_for` is the chat to get the integration for.
30    pub async fn init_webxdc_integration(
31        &self,
32        integrate_for: Option<ChatId>,
33    ) -> Result<Option<MsgId>> {
34        let Some(instance_id) = self
35            .get_config_parsed::<u32>(Config::WebxdcIntegration)
36            .await?
37        else {
38            return Ok(None);
39        };
40
41        let Some(mut instance) =
42            Message::load_from_db_optional(self, MsgId::new(instance_id)).await?
43        else {
44            return Ok(None);
45        };
46
47        if instance.viewtype != Viewtype::Webxdc {
48            return Ok(None);
49        }
50
51        let integrate_for = integrate_for.unwrap_or_default().to_u32() as i32;
52        if instance.param.get_int(Param::WebxdcIntegrateFor) != Some(integrate_for) {
53            instance
54                .param
55                .set_int(Param::WebxdcIntegrateFor, integrate_for);
56            instance.update_param(self).await?;
57        }
58        Ok(Some(instance.id))
59    }
60
61    // Check if a Webxdc shall be used as an integration and remember that.
62    pub(crate) async fn update_webxdc_integration_database(
63        &self,
64        msg: &mut Message,
65        context: &Context,
66    ) -> Result<()> {
67        if msg.viewtype == Viewtype::Webxdc {
68            let is_integration = if msg.param.get_int(Param::WebxdcIntegration).is_some() {
69                true
70            } else if msg.chat_id.is_self_talk(context).await? {
71                let info = msg.get_webxdc_info(context).await?;
72                if info.request_integration == "map" {
73                    msg.param.set_int(Param::WebxdcIntegration, 1);
74                    msg.update_param(context).await?;
75                    true
76                } else {
77                    false
78                }
79            } else {
80                false
81            };
82
83            if is_integration {
84                self.set_config_internal(
85                    Config::WebxdcIntegration,
86                    Some(&msg.id.to_u32().to_string()),
87                )
88                .await?;
89            }
90        }
91        Ok(())
92    }
93
94    // Intercepts sending updates from Webxdc to core.
95    pub(crate) async fn intercept_send_webxdc_status_update(
96        &self,
97        instance: Message,
98        status_update: StatusUpdateItem,
99    ) -> Result<()> {
100        let chat_id = instance.webxdc_integrated_for();
101        maps_integration::intercept_send_update(self, chat_id, status_update).await
102    }
103
104    // Intercepts Webxdc requesting updates from core.
105    pub(crate) async fn intercept_get_webxdc_status_updates(
106        &self,
107        instance: Message,
108        last_known_serial: StatusUpdateSerial,
109    ) -> Result<String> {
110        let chat_id = instance.webxdc_integrated_for();
111        maps_integration::intercept_get_updates(self, chat_id, last_known_serial).await
112    }
113}
114
115impl Message {
116    // Get chat the Webxdc is integrated for.
117    // This is the chat given to `init_webxdc_integration()`.
118    fn webxdc_integrated_for(&self) -> Option<ChatId> {
119        let raw_id = self.param.get_int(Param::WebxdcIntegrateFor).unwrap_or(0) as u32;
120        if raw_id > 0 {
121            Some(ChatId::new(raw_id))
122        } else {
123            None
124        }
125    }
126
127    // Check if the message is an actually used as Webxdc integration.
128    pub(crate) async fn is_set_as_webxdc_integration(&self, context: &Context) -> Result<bool> {
129        if let Some(integration_id) = context
130            .get_config_parsed::<u32>(Config::WebxdcIntegration)
131            .await?
132        {
133            Ok(integration_id == self.id.to_u32())
134        } else {
135            Ok(false)
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use crate::config::Config;
143    use crate::context::Context;
144    use crate::message;
145    use crate::message::{Message, Viewtype};
146    use crate::test_utils::TestContext;
147    use anyhow::Result;
148    use std::time::Duration;
149
150    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
151    async fn test_default_integrations_are_single_device() -> Result<()> {
152        let t = TestContext::new_alice().await;
153        t.set_config_bool(Config::BccSelf, false).await?;
154
155        let bytes = include_bytes!("../../test-data/webxdc/minimal.xdc");
156        let file = t.get_blobdir().join("maps.xdc");
157        tokio::fs::write(&file, bytes).await.unwrap();
158        t.set_webxdc_integration(file.to_str().unwrap()).await?;
159
160        // default integrations are shipped with the apps and should not be sent over the wire
161        let sent = t.pop_sent_msg_opt(Duration::from_secs(1)).await;
162        assert!(sent.is_none());
163
164        Ok(())
165    }
166
167    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
168    async fn test_overwrite_default_integration() -> Result<()> {
169        let t = TestContext::new_alice().await;
170        let self_chat = &t.get_self_chat().await;
171        assert!(t.init_webxdc_integration(None).await?.is_none());
172
173        async fn assert_integration(t: &Context, name: &str) -> Result<()> {
174            let integration_id = t.init_webxdc_integration(None).await?.unwrap();
175            let integration = Message::load_from_db(t, integration_id).await?;
176            let integration_info = integration.get_webxdc_info(t).await?;
177            assert_eq!(integration_info.name, name);
178            Ok(())
179        }
180
181        // set default integration
182        let bytes = include_bytes!("../../test-data/webxdc/with-manifest-and-png-icon.xdc");
183        let file = t.get_blobdir().join("maps.xdc");
184        tokio::fs::write(&file, bytes).await.unwrap();
185        t.set_webxdc_integration(file.to_str().unwrap()).await?;
186        assert_integration(&t, "with some icon").await?;
187
188        // send a maps.xdc with insufficient manifest
189        let mut msg = Message::new(Viewtype::Webxdc);
190        msg.set_file_from_bytes(
191            &t,
192            "mapstest.xdc",
193            include_bytes!("../../test-data/webxdc/mapstest-integration-unset.xdc"),
194            None,
195        )?;
196        t.send_msg(self_chat.id, &mut msg).await;
197        assert_integration(&t, "with some icon").await?; // still the default integration
198
199        // send a maps.xdc with manifest including the line `request_integration = "map"`
200        let mut msg = Message::new(Viewtype::Webxdc);
201        msg.set_file_from_bytes(
202            &t,
203            "mapstest.xdc",
204            include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"),
205            None,
206        )?;
207        let sent = t.send_msg(self_chat.id, &mut msg).await;
208        let info = msg.get_webxdc_info(&t).await?;
209        assert!(info.summary.contains("Used as map"));
210        assert_integration(&t, "Maps Test 2").await?;
211
212        // when maps.xdc is received on another device, the integration is not accepted (needs to be forwarded again)
213        let t2 = TestContext::new_alice().await;
214        let msg2 = t2.recv_msg(&sent).await;
215        let info = msg2.get_webxdc_info(&t2).await?;
216        assert!(info.summary.contains("To use as map,"));
217        assert!(t2.init_webxdc_integration(None).await?.is_none());
218
219        // deleting maps.xdc removes the user's integration - the UI will go back to default calling set_webxdc_integration() then
220        message::delete_msgs(&t, &[msg.id]).await?;
221        assert!(t.init_webxdc_integration(None).await?.is_none());
222
223        Ok(())
224    }
225}