diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 4ef7b5b22..b3951aa38 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1175,6 +1175,24 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const */ char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial); + +/** + * Replaces webxdc app with a new version. + * + * On the JavaScript side this API could be used like this: + * ``` + * window.webxdc.replaceWebxdc(blob); + * ``` + * + * @memberof dc_context_t + * @param context The context object. + * @param msg_id The ID of the WebXDC message to be replaced. + * @param blob New blob to replace WebXDC with. + * @param n Blob size. + */ +void dc_replace_webxdc(dc_context_t* context, uint32_t msg_id, uint8_t *blob, size_t n); + + /** * Save a draft for a chat in the database. * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 7548b8a1c..593b12525 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -36,7 +36,7 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions}; use deltachat::stock_str::StockMessage; use deltachat::stock_str::StockStrings; -use deltachat::webxdc::StatusUpdateSerial; +use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial}; use deltachat::*; use deltachat::{accounts::Accounts, log::LogExt}; use num_traits::{FromPrimitive, ToPrimitive}; @@ -1097,6 +1097,32 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates( .strdup() } +#[no_mangle] +pub unsafe extern "C" fn dc_replace_webxdc( + context: *mut dc_context_t, + msg_id: u32, + blob: *const u8, + n: libc::size_t, +) { + if context.is_null() { + eprintln!("ignoring careless call to dc_replace_webxdc()"); + return; + } + + let msg_id = MsgId::new(msg_id); + let blob_slice = std::slice::from_raw_parts(blob, n); + + let ctx = &*context; + + block_on(async move { + replace_webxdc(ctx, msg_id, blob_slice) + .await + .context("Failed to replace WebXDC") + .log_err(ctx) + .ok(); + }) +} + #[no_mangle] pub unsafe extern "C" fn dc_set_draft( context: *mut dc_context_t, @@ -1539,14 +1565,10 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32 } let ctx = &*context; - block_on(async move { - ChatId::new(chat_id) - .delete(ctx) - .await - .context("Failed chat delete") - .log_err(ctx) - .ok(); - }) + block_on(ChatId::new(chat_id).delete(ctx)) + .context("Failed chat delete") + .log_err(ctx) + .ok(); } #[no_mangle] diff --git a/src/webxdc.rs b/src/webxdc.rs index 4697f025b..eefc6a310 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -26,6 +26,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::io::AsyncReadExt; +use crate::blob::BlobObject; use crate::chat::Chat; use crate::constants::Chattype; use crate::contact::ContactId; @@ -845,6 +846,35 @@ impl Message { } } +/// Replaces WebXDC blob of existing message. +/// +/// This API is supposed to be called from within a WebXDC to replace itself +/// e.g. with an updated or persistently reconfigured version. +pub async fn replace_webxdc(context: &Context, msg_id: MsgId, data: &[u8]) -> Result<()> { + let mut msg = Message::load_from_db(context, msg_id).await?; + + ensure!( + msg.get_viewtype() == Viewtype::Webxdc, + "Message {msg_id} is not a WebXDC instance" + ); + + let blob = BlobObject::create( + context, + &msg.get_filename() + .context("Cannot get filename of exising WebXDC instance")?, + data, + ) + .await + .context("Failed to create WebXDC replacement blob")?; + + let mut param = msg.param.clone(); + param.set(Param::File, blob.as_name()); + msg.param = param; + msg.update_param(context).await?; + + Ok(()) +} + #[cfg(test)] mod tests { use serde_json::json; @@ -2623,4 +2653,62 @@ sth_for_the = "future""# Ok(()) } + + /// Tests replacing WebXDC with a newer version. + /// + /// Updates should be preserved after upgrading. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_replace_webxdc() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice sends WebXDC instance. + let alice_chat = alice.create_chat(&bob).await; + let mut alice_instance = create_webxdc_instance( + &alice, + "minimal.xdc", + include_bytes!("../test-data/webxdc/minimal.xdc"), + ) + .await?; + alice_instance.set_text("user added text".to_string()); + send_msg(&alice, alice_chat.id, &mut alice_instance).await?; + let alice_instance = alice.get_last_msg().await; + assert_eq!(alice_instance.get_text(), "user added text"); + + // Bob receives that instance. + let alice_sent_instance = alice.pop_sent_msg().await; + let bob_received_instance = bob.recv_msg(&alice_sent_instance).await; + assert_eq!(bob_received_instance.get_text(), "user added text"); + + // Alice sends WebXDC update. + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload": 1}"#, "Alice update") + .await?; + alice.flush_status_updates().await?; + let alice_sent_update = alice.pop_sent_msg().await; + bob.recv_msg(&alice_sent_update).await; + assert_eq!( + bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":1,"serial":1,"max_serial":1}]"# + ); + + // Bob replaces WebXDC. + replace_webxdc( + &bob, + bob_received_instance.id, + include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"), + ) + .await + .context("Failed to replace WebXDC")?; + + // Updates are not modified. + assert_eq!( + bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":1,"serial":1,"max_serial":1}]"# + ); + + Ok(()) + } }