diff --git a/assets/icon-webxdc.png b/assets/icon-webxdc.png
new file mode 100644
index 000000000..87c5fa583
Binary files /dev/null and b/assets/icon-webxdc.png differ
diff --git a/assets/icon-webxdc.svg b/assets/icon-webxdc.svg
new file mode 100644
index 000000000..1164bc39d
--- /dev/null
+++ b/assets/icon-webxdc.svg
@@ -0,0 +1,96 @@
+
+
diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h
index 3fc5c130c..0736b2415 100644
--- a/deltachat-ffi/deltachat.h
+++ b/deltachat-ffi/deltachat.h
@@ -3689,6 +3689,27 @@ char* dc_msg_get_filemime (const dc_msg_t* msg);
char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char* filename, size_t* ret_bytes);
+/**
+ * Get info a webxdc message, in JSON format.
+ * The returned JSON string has the following key/values:
+ *
+ * - name: The name of the app.
+ * Defaults to the filename if not set in the manifest.
+ * - icon: App icon file name.
+ * Defaults to an standard icon if nothing is set in the manifest.
+ * To get the file, use dc_msg_get_webxdc_blob().
+ * App icons should should be square,
+ * the implementations will add round corners etc. as needed.
+ *
+ * @memberof dc_msg_t
+ * @param msg The webxdc instance.
+ * @return a UTF8-encoded JSON string containing all requested info.
+ * Must be freed using dc_str_unref().
+ * NULL is never returned.
+ */
+char* dc_msg_get_webxdc_info (const dc_msg_t* msg);
+
+
/**
* Get the size of the file. Returns the size of the file associated with a
* message, if applicable.
diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs
index dd850f2b7..1585432b4 100644
--- a/deltachat-ffi/src/lib.rs
+++ b/deltachat-ffi/src/lib.rs
@@ -3090,7 +3090,6 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
});
match blob {
Ok(blob) => {
- // TODO: introduce dc_blob_t to avoid malloc and returning size by pointer and to save copying data
*ret_bytes = blob.len();
let ptr = libc::malloc(*ret_bytes);
libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes);
@@ -3103,6 +3102,29 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
}
}
+#[no_mangle]
+pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc::c_char {
+ if msg.is_null() {
+ eprintln!("ignoring careless call to dc_msg_get_webxdc_info()");
+ return "".strdup();
+ }
+ let ffi_msg = &*msg;
+ let ctx = &*ffi_msg.context;
+
+ block_on(async move {
+ let info = match ffi_msg.message.get_webxdc_info(ctx).await {
+ Ok(info) => info,
+ Err(err) => {
+ error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {}", err);
+ return "".strdup();
+ }
+ };
+ serde_json::to_string(&info)
+ .unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
+ .strdup()
+ })
+}
+
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
diff --git a/src/webxdc.rs b/src/webxdc.rs
index 14142469c..f0e451f7a 100644
--- a/src/webxdc.rs
+++ b/src/webxdc.rs
@@ -16,6 +16,22 @@ use std::convert::TryFrom;
use std::io::Read;
pub const WEBXDC_SUFFIX: &str = "xdc";
+const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
+
+/// Raw information read from manifest.toml
+#[derive(Debug, Deserialize)]
+#[non_exhaustive]
+struct WebxdcManifest {
+ name: Option,
+ icon: Option,
+}
+
+/// Parsed information from WebxdcManifest and fallbacks.
+#[derive(Debug, Serialize)]
+pub struct WebxdcInfo {
+ pub name: String,
+ pub icon: String,
+}
/// Status Update ID.
#[derive(
@@ -227,12 +243,21 @@ impl Context {
}
}
+async fn parse_webxdc_manifest(bytes: &[u8]) -> Result {
+ let manifest: WebxdcManifest = toml::from_slice(bytes)?;
+ Ok(manifest)
+}
+
impl Message {
/// Return file form inside an archive.
/// Currently, this works only if the message is an webxdc instance.
pub async fn get_webxdc_blob(&self, context: &Context, name: &str) -> Result> {
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
+ if name == WEBXDC_DEFAULT_ICON {
+ return Ok(include_bytes!("../assets/icon-webxdc.png").to_vec());
+ }
+
let archive = self
.get_file(context)
.ok_or_else(|| format_err!("No webxdc instance file."))?;
@@ -253,6 +278,58 @@ impl Message {
file.read_to_end(&mut buf)?;
Ok(buf)
}
+
+ /// Return info from manifest.toml or from fallbacks.
+ pub async fn get_webxdc_info(&self, context: &Context) -> Result {
+ ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
+
+ let mut manifest = if let Ok(bytes) = self.get_webxdc_blob(context, "manifest.toml").await {
+ if let Ok(manifest) = parse_webxdc_manifest(&bytes).await {
+ manifest
+ } else {
+ WebxdcManifest {
+ name: None,
+ icon: None,
+ }
+ }
+ } else {
+ WebxdcManifest {
+ name: None,
+ icon: None,
+ }
+ };
+
+ if let Some(ref name) = manifest.name {
+ let name = name.trim();
+ if name.is_empty() {
+ warn!(context, "empty name given in manifest");
+ manifest.name = None;
+ }
+ }
+
+ if let Some(ref icon) = manifest.icon {
+ if !icon.ends_with(".png") && !icon.ends_with(".jpg") {
+ warn!(context, "bad icon format \"{}\"; use .png or .jpg", icon);
+ manifest.icon = None;
+ } else if self.get_webxdc_blob(context, icon).await.is_err() {
+ warn!(context, "cannot find icon \"{}\"", icon);
+ manifest.icon = None;
+ }
+ }
+
+ Ok(WebxdcInfo {
+ name: if let Some(name) = manifest.name {
+ name
+ } else {
+ self.get_filename().unwrap_or_default()
+ },
+ icon: if let Some(icon) = manifest.icon {
+ icon
+ } else {
+ WEBXDC_DEFAULT_ICON.to_string()
+ },
+ })
+ }
}
#[cfg(test)]
@@ -305,19 +382,21 @@ mod tests {
Ok(())
}
- async fn create_webxdc_instance(t: &TestContext) -> Result {
- let file = t.get_blobdir().join("minimal.xdc");
- File::create(&file)
- .await?
- .write_all(include_bytes!("../test-data/webxdc/minimal.xdc"))
- .await?;
+ async fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result {
+ let file = t.get_blobdir().join(name);
+ File::create(&file).await?.write_all(bytes).await?;
let mut instance = Message::new(Viewtype::File);
instance.set_file(file.to_str().unwrap(), None);
Ok(instance)
}
async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result {
- let mut instance = create_webxdc_instance(t).await?;
+ let mut instance = create_webxdc_instance(
+ t,
+ "minimal.xdc",
+ include_bytes!("../test-data/webxdc/minimal.xdc"),
+ )
+ .await?;
let instance_msg_id = send_msg(t, chat_id, &mut instance).await?;
Message::load_from_db(t, instance_msg_id).await
}
@@ -379,7 +458,12 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
- let mut instance = create_webxdc_instance(&t).await?;
+ let mut instance = create_webxdc_instance(
+ &t,
+ "minimal.xdc",
+ include_bytes!("../test-data/webxdc/minimal.xdc"),
+ )
+ .await?;
chat_id.set_draft(&t, Some(&mut instance)).await?;
let instance = chat_id.get_draft(&t).await?.unwrap();
t.send_webxdc_status_update(instance.id, "descr", "42")
@@ -603,7 +687,12 @@ mod tests {
// prepare webxdc instance,
// status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None)
- let mut alice_instance = create_webxdc_instance(&alice).await?;
+ let mut alice_instance = create_webxdc_instance(
+ &alice,
+ "minimal.xdc",
+ include_bytes!("../test-data/webxdc/minimal.xdc"),
+ )
+ .await?;
alice_chat_id
.set_draft(&alice, Some(&mut alice_instance))
.await?;
@@ -677,6 +766,18 @@ mod tests {
Ok(())
}
+ #[async_std::test]
+ async fn test_get_webxdc_blob_default_icon() -> Result<()> {
+ let t = TestContext::new_alice().await;
+ let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
+ let instance = send_webxdc_instance(&t, chat_id).await?;
+
+ let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?;
+ assert!(buf.len() > 100);
+ assert!(String::from_utf8_lossy(&buf).contains("PNG\r\n"));
+ Ok(())
+ }
+
#[async_std::test]
async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -694,13 +795,12 @@ mod tests {
async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
- let file = t.get_blobdir().join("some-files.xdc");
- File::create(&file)
- .await?
- .write_all(include_bytes!("../test-data/webxdc/some-files.xdc"))
- .await?;
- let mut instance = Message::new(Viewtype::Webxdc);
- instance.set_file(file.to_str().unwrap(), None);
+ let mut instance = create_webxdc_instance(
+ &t,
+ "some-files.xdc",
+ include_bytes!("../test-data/webxdc/some-files.xdc"),
+ )
+ .await?;
chat_id.set_draft(&t, Some(&mut instance)).await?;
let buf = instance.get_webxdc_blob(&t, "index.html").await?;
@@ -731,4 +831,126 @@ mod tests {
Ok(())
}
+
+ #[async_std::test]
+ async fn test_parse_webxdc_manifest() -> Result<()> {
+ let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes()).await;
+ assert!(result.is_err());
+
+ let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes()).await?;
+ assert_eq!(manifest.name, None);
+ assert_eq!(manifest.icon, None);
+
+ let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes()).await?;
+ assert_eq!(manifest.name, Some("name, no icon".to_string()));
+ assert_eq!(manifest.icon, None);
+
+ let manifest = parse_webxdc_manifest(
+ r#"name = "foo"
+icon = "bar""#
+ .as_bytes(),
+ )
+ .await?;
+ assert_eq!(manifest.name, Some("foo".to_string()));
+ assert_eq!(manifest.icon, Some("bar".to_string()));
+
+ let manifest = parse_webxdc_manifest(
+ r#"name = "foz"
+icon = "baz"
+add_item = "that should be just ignored"
+
+[section]
+sth_for_the = "future""#
+ .as_bytes(),
+ )
+ .await?;
+ assert_eq!(manifest.name, Some("foz".to_string()));
+ assert_eq!(manifest.icon, Some("baz".to_string()));
+
+ Ok(())
+ }
+
+ #[async_std::test]
+ async fn test_get_webxdc_info() -> Result<()> {
+ let t = TestContext::new_alice().await;
+ let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
+
+ let instance = send_webxdc_instance(&t, chat_id).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "minimal.xdc");
+ assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
+
+ let mut instance = create_webxdc_instance(
+ &t,
+ "with-manifest-empty-name.xdc",
+ include_bytes!("../test-data/webxdc/with-manifest-empty-name.xdc"),
+ )
+ .await?;
+ chat_id.set_draft(&t, Some(&mut instance)).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "with-manifest-empty-name.xdc");
+ assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
+
+ let mut instance = create_webxdc_instance(
+ &t,
+ "with-manifest-no-name.xdc",
+ include_bytes!("../test-data/webxdc/with-manifest-no-name.xdc"),
+ )
+ .await?;
+ chat_id.set_draft(&t, Some(&mut instance)).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "with-manifest-no-name.xdc");
+ assert_eq!(info.icon, "some.png".to_string());
+
+ let mut instance = create_webxdc_instance(
+ &t,
+ "with-minimal-manifest.xdc",
+ include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
+ )
+ .await?;
+ chat_id.set_draft(&t, Some(&mut instance)).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "nice app!");
+ assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
+
+ let mut instance = create_webxdc_instance(
+ &t,
+ "with-manifest-icon-not-existent.xdc",
+ include_bytes!("../test-data/webxdc/with-manifest-icon-not-existent.xdc"),
+ )
+ .await?;
+ chat_id.set_draft(&t, Some(&mut instance)).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "with bad icon");
+ assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
+
+ let mut instance = create_webxdc_instance(
+ &t,
+ "with-manifest-and-icon.xdc",
+ include_bytes!("../test-data/webxdc/with-manifest-and-icon.xdc"),
+ )
+ .await?;
+ chat_id.set_draft(&t, Some(&mut instance)).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "with some icon");
+ assert_eq!(info.icon, "some.png");
+
+ let mut instance = create_webxdc_instance(
+ &t,
+ "with-manifest-and-unsupported-icon-format.xdc",
+ include_bytes!("../test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc"),
+ )
+ .await?;
+ chat_id.set_draft(&t, Some(&mut instance)).await?;
+ let info = instance.get_webxdc_info(&t).await?;
+ assert_eq!(info.name, "with tiff icon");
+ assert_eq!(info.icon, WEBXDC_DEFAULT_ICON);
+
+ let msg_id = send_text_msg(&t, chat_id, "foo".to_string()).await?;
+ let msg = Message::load_from_db(&t, msg_id).await?;
+ let result = msg.get_webxdc_info(&t).await;
+ assert!(result.is_err());
+
+ Ok(())
+ }
}
diff --git a/test-data/webxdc/with-manifest-and-icon.xdc b/test-data/webxdc/with-manifest-and-icon.xdc
new file mode 100644
index 000000000..96291b470
Binary files /dev/null and b/test-data/webxdc/with-manifest-and-icon.xdc differ
diff --git a/test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc b/test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc
new file mode 100644
index 000000000..26785b345
Binary files /dev/null and b/test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc differ
diff --git a/test-data/webxdc/with-manifest-empty-name.xdc b/test-data/webxdc/with-manifest-empty-name.xdc
new file mode 100644
index 000000000..f720a1269
Binary files /dev/null and b/test-data/webxdc/with-manifest-empty-name.xdc differ
diff --git a/test-data/webxdc/with-manifest-icon-not-existent.xdc b/test-data/webxdc/with-manifest-icon-not-existent.xdc
new file mode 100644
index 000000000..1697cbcd6
Binary files /dev/null and b/test-data/webxdc/with-manifest-icon-not-existent.xdc differ
diff --git a/test-data/webxdc/with-manifest-no-name.xdc b/test-data/webxdc/with-manifest-no-name.xdc
new file mode 100644
index 000000000..d25767ada
Binary files /dev/null and b/test-data/webxdc/with-manifest-no-name.xdc differ
diff --git a/test-data/webxdc/with-minimal-manifest.xdc b/test-data/webxdc/with-minimal-manifest.xdc
new file mode 100644
index 000000000..76925ffa8
Binary files /dev/null and b/test-data/webxdc/with-minimal-manifest.xdc differ