From d63a2b39aad4816cf6110cfd88b92dc23f6518c8 Mon Sep 17 00:00:00 2001 From: bjoern Date: Fri, 29 Nov 2024 15:18:35 +0100 Subject: [PATCH] feat: allow the user to replace maps integration (#5678) with this PR, when an `.xdc` with `request_integration = map` in the manifest is added to the "Saved Messages" chat, it is used _locally_ as an replacement for the shipped maps.xdc (other devices will see the `.xdc` but not use it) this allows easy development and adapting the map to use services that work better in some area. there are lots of known discussions and ideas about adding more barriers of safety. however, after internal discussions, we decided to move forward and also to allow internet, if requested by an integration (as discussed at https://github.com/deltachat/deltachat-core-rust/pull/3516). the gist is to ease development and to make users who want to adapt, actionable _now_, without making things too hard and adding too high barriers or stressing our own resources/power too much. note, that things are still experimental and will be the next time - without the corresponding switch being enabled, nothing will work at all, so we can be quite relaxed here :) for android/ios, things will work directly. for desktop, allow_internet needs to be accepted unconditionally from core. for the future, we might add a question before using an integration and/or add signing. or sth. completely different - but for now, the thing is to get started. nb: "integration" field in the webxdc-info is experimental as well and should not be used in UIs at all currently, it may vanish again and is there mainly for simplicity of the code; therefore, no need to document that. successor of https://github.com/deltachat/deltachat-core-rust/pull/5461 this is how it looks like currently - again, please note that all that is an experiment!     ... when going out of experimental, there are loots of ideas, eg. changing "Start" to "integrate" --- deltachat-jsonrpc/src/api/types/webxdc.rs | 1 + src/chat.rs | 8 +- src/webxdc.rs | 17 ++- src/webxdc/integration.rs | 112 ++++++++++++++++-- src/webxdc/maps_integration.rs | 4 +- test-data/webxdc/mapstest-integration-set.xdc | Bin 0 -> 409 bytes ...est.xdc => mapstest-integration-unset.xdc} | Bin 7 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 test-data/webxdc/mapstest-integration-set.xdc rename test-data/webxdc/{mapstest.xdc => mapstest-integration-unset.xdc} (100%) diff --git a/deltachat-jsonrpc/src/api/types/webxdc.rs b/deltachat-jsonrpc/src/api/types/webxdc.rs index e942d7be2f..28d27a47ad 100644 --- a/deltachat-jsonrpc/src/api/types/webxdc.rs +++ b/deltachat-jsonrpc/src/api/types/webxdc.rs @@ -57,6 +57,7 @@ impl WebxdcMessageInfo { document, summary, source_code_url, + request_integration: _, internet_access, self_addr, send_update_interval, diff --git a/src/chat.rs b/src/chat.rs index f68c292db4..46d8e3a6ee 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2202,7 +2202,9 @@ impl Chat { msg.id = MsgId::new(u32::try_from(raw_id)?); maybe_set_logging_xdc(context, msg, self.id).await?; - context.update_webxdc_integration_database(msg).await?; + context + .update_webxdc_integration_database(msg, context) + .await?; } context.scheduler.interrupt_ephemeral_task().await; Ok(msg.id) @@ -2977,8 +2979,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - recipients.push(from); } - // Webxdc integrations are messages, however, shipped with main app and must not be sent out - if msg.param.get_int(Param::WebxdcIntegration).is_some() { + // Default Webxdc integrations are hidden messages and must not be sent out + if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden { recipients.clear(); } diff --git a/src/webxdc.rs b/src/webxdc.rs index 5c0b01535a..c4abad2297 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -74,6 +74,9 @@ pub struct WebxdcManifest { /// Optional URL of webxdc source code. pub source_code_url: Option, + /// Set to "map" to request integration. + pub request_integration: Option, + /// If the webxdc requests network access. pub request_internet_access: Option, } @@ -100,6 +103,9 @@ pub struct WebxdcInfo { /// URL of webxdc source code or an empty string. pub source_code_url: String, + /// Set to "map" to request integration, otherwise an empty string. + pub request_integration: String, + /// If the webxdc is allowed to access the network. /// It should request access, be encrypted /// and sent to self for this. @@ -920,6 +926,9 @@ impl Message { } } + let request_integration = manifest.request_integration.unwrap_or_default(); + let is_integrated = self.is_set_as_webxdc_integration(context).await?; + let internet_access = manifest.request_internet_access.unwrap_or_default() && self.chat_id.is_self_talk(context).await.unwrap_or_default() && self.get_showpadlock(); @@ -944,7 +953,12 @@ impl Message { .get(Param::WebxdcDocument) .unwrap_or_default() .to_string(), - summary: if internet_access { + summary: if is_integrated { + "🌍 Used as map. Delete to use default. Do not enter sensitive data".to_string() + } else if request_integration == "map" { + "🌏 To use as map, forward to \"Saved Messages\" again. Do not enter sensitive data" + .to_string() + } else if internet_access { "Dev Mode: Do not enter sensitive data!".to_string() } else { self.param @@ -957,6 +971,7 @@ impl Message { } else { "".to_string() }, + request_integration, internet_access, self_addr, send_update_interval: context.ratelimit.read().await.update_interval(), diff --git a/src/webxdc/integration.rs b/src/webxdc/integration.rs index 500cd36bf6..b07600def6 100644 --- a/src/webxdc/integration.rs +++ b/src/webxdc/integration.rs @@ -57,14 +57,34 @@ impl Context { } // Check if a Webxdc shall be used as an integration and remember that. - pub(crate) async fn update_webxdc_integration_database(&self, msg: &Message) -> Result<()> { - if msg.viewtype == Viewtype::Webxdc && msg.param.get_int(Param::WebxdcIntegration).is_some() - { - self.set_config_internal( - Config::WebxdcIntegration, - Some(&msg.id.to_u32().to_string()), - ) - .await?; + pub(crate) async fn update_webxdc_integration_database( + &self, + msg: &mut Message, + context: &Context, + ) -> Result<()> { + if msg.viewtype == Viewtype::Webxdc { + let is_integration = if msg.param.get_int(Param::WebxdcIntegration).is_some() { + true + } else if msg.chat_id.is_self_talk(context).await? { + let info = msg.get_webxdc_info(context).await?; + if info.request_integration == "map" { + msg.param.set_int(Param::WebxdcIntegration, 1); + msg.update_param(context).await?; + true + } else { + false + } + } else { + false + }; + + if is_integration { + self.set_config_internal( + Config::WebxdcIntegration, + Some(&msg.id.to_u32().to_string()), + ) + .await?; + } } Ok(()) } @@ -101,11 +121,26 @@ impl Message { None } } + + // Check if the message is an actually used as Webxdc integration. + pub(crate) async fn is_set_as_webxdc_integration(&self, context: &Context) -> Result { + if let Some(integration_id) = context + .get_config_parsed::(Config::WebxdcIntegration) + .await? + { + Ok(integration_id == self.id.to_u32()) + } else { + Ok(false) + } + } } #[cfg(test)] mod tests { use crate::config::Config; + use crate::context::Context; + use crate::message; + use crate::message::{Message, Viewtype}; use crate::test_utils::TestContext; use anyhow::Result; use std::time::Duration; @@ -126,4 +161,65 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_overwrite_default_integration() -> Result<()> { + let t = TestContext::new_alice().await; + let self_chat = &t.get_self_chat().await; + assert!(t.init_webxdc_integration(None).await?.is_none()); + + async fn assert_integration(t: &Context, name: &str) -> Result<()> { + let integration_id = t.init_webxdc_integration(None).await?.unwrap(); + let integration = Message::load_from_db(t, integration_id).await?; + let integration_info = integration.get_webxdc_info(t).await?; + assert_eq!(integration_info.name, name); + Ok(()) + } + + // set default integration + let bytes = include_bytes!("../../test-data/webxdc/with-manifest-and-png-icon.xdc"); + let file = t.get_blobdir().join("maps.xdc"); + tokio::fs::write(&file, bytes).await.unwrap(); + t.set_webxdc_integration(file.to_str().unwrap()).await?; + assert_integration(&t, "with some icon").await?; + + // send a maps.xdc with insufficient manifest + let mut msg = Message::new(Viewtype::Webxdc); + msg.set_file_from_bytes( + &t, + "mapstest.xdc", + include_bytes!("../../test-data/webxdc/mapstest-integration-unset.xdc"), + None, + ) + .await?; + t.send_msg(self_chat.id, &mut msg).await; + assert_integration(&t, "with some icon").await?; // still the default integration + + // send a maps.xdc with manifest including the line `request_integration = "map"` + let mut msg = Message::new(Viewtype::Webxdc); + msg.set_file_from_bytes( + &t, + "mapstest.xdc", + include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"), + None, + ) + .await?; + let sent = t.send_msg(self_chat.id, &mut msg).await; + let info = msg.get_webxdc_info(&t).await?; + assert!(info.summary.contains("Used as map")); + assert_integration(&t, "Maps Test 2").await?; + + // when maps.xdc is received on another device, the integration is not accepted (needs to be forwarded again) + let t2 = TestContext::new_alice().await; + let msg2 = t2.recv_msg(&sent).await; + let info = msg2.get_webxdc_info(&t2).await?; + assert!(info.summary.contains("To use as map,")); + assert!(t2.init_webxdc_integration(None).await?.is_none()); + + // deleting maps.xdc removes the user's integration - the UI will go back to default calling set_webxdc_integration() then + message::delete_msgs(&t, &[msg.id]).await?; + assert!(t.init_webxdc_integration(None).await?.is_none()); + + Ok(()) + } } diff --git a/src/webxdc/maps_integration.rs b/src/webxdc/maps_integration.rs index 67ff755607..a1d2161f0f 100644 --- a/src/webxdc/maps_integration.rs +++ b/src/webxdc/maps_integration.rs @@ -181,7 +181,7 @@ mod tests { async fn test_maps_integration() -> Result<()> { let t = TestContext::new_alice().await; - let bytes = include_bytes!("../../test-data/webxdc/mapstest.xdc"); + let bytes = include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"); let file = t.get_blobdir().join("maps.xdc"); tokio::fs::write(&file, bytes).await.unwrap(); t.set_webxdc_integration(file.to_str().unwrap()).await?; @@ -199,7 +199,7 @@ mod tests { let integration = Message::load_from_db(&t, integration_id).await?; let info = integration.get_webxdc_info(&t).await?; - assert_eq!(info.name, "Maps Test"); + assert_eq!(info.name, "Maps Test 2"); assert_eq!(info.internet_access, true); t.send_webxdc_status_update( diff --git a/test-data/webxdc/mapstest-integration-set.xdc b/test-data/webxdc/mapstest-integration-set.xdc new file mode 100644 index 0000000000000000000000000000000000000000..e25819ce95d78da3c5922fcd24dd5a3dabd91249 GIT binary patch literal 409 zcmWIWW@h1HU}9ikNX$4Eq0+*8T?EJmVJ-$4hRnQ_)C#?flH8oo5Kac>wCRayiC)QR zr4`%^j4WRn85meVDs2>9{GCH016&nAD($%PGApmf5vVc% zhp&LR2KKbfsgmHN4>IE7EJ<{^h)nPSy zZ2Z|P=yJD^x6$X%4p~LQ+yPI!J_UI{5}rIcxVyXYld9{|l`#wf-i%Cg%(#3g0dx-o z1JEA~OBz8eWdE{4{EOySh