1mod integration;
19mod maps_integration;
20
21use std::cmp::max;
22use std::collections::HashMap;
23use std::path::Path;
24
25use anyhow::{Context as _, Result, anyhow, bail, ensure, format_err};
26
27use async_zip::tokio::read::seek::ZipFileReader as SeekZipFileReader;
28use deltachat_contact_tools::sanitize_bidi_characters;
29use deltachat_derive::FromSql;
30use mail_builder::mime::MimePart;
31use rusqlite::OptionalExtension;
32use serde::{Deserialize, Serialize};
33use serde_json::Value;
34use sha2::{Digest, Sha256};
35use tokio::{fs::File, io::BufReader};
36
37use crate::chat::{self, Chat};
38use crate::constants::Chattype;
39use crate::contact::ContactId;
40use crate::context::Context;
41use crate::events::EventType;
42use crate::key::self_fingerprint;
43use crate::log::warn;
44use crate::message::{Message, MessageState, MsgId, Viewtype};
45use crate::mimefactory::RECOMMENDED_FILE_SIZE;
46use crate::mimeparser::SystemMessage;
47use crate::param::Param;
48use crate::param::Params;
49use crate::tools::{create_id, create_smeared_timestamp, get_abs_path};
50
51const WEBXDC_API_VERSION: u32 = 1;
56
57pub const WEBXDC_SUFFIX: &str = "xdc";
59const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
60
61const BODY_DESCR: &str = "Webxdc Status Update";
63
64#[derive(Debug, Deserialize, Default)]
66#[non_exhaustive]
67pub struct WebxdcManifest {
68 pub name: Option<String>,
70
71 pub min_api: Option<u32>,
73
74 pub source_code_url: Option<String>,
76
77 pub request_integration: Option<String>,
79}
80
81#[derive(Debug, Serialize)]
83pub struct WebxdcInfo {
84 pub name: String,
87
88 pub icon: String,
90
91 pub document: String,
95
96 pub summary: String,
99
100 pub source_code_url: String,
102
103 pub request_integration: String,
105
106 pub internet_access: bool,
110
111 pub self_addr: String,
113
114 pub send_update_interval: usize,
117
118 pub send_update_max_size: usize,
121}
122
123#[derive(
125 Debug,
126 Copy,
127 Clone,
128 Default,
129 PartialEq,
130 Eq,
131 Hash,
132 PartialOrd,
133 Ord,
134 Serialize,
135 Deserialize,
136 FromSql,
137 FromPrimitive,
138)]
139pub struct StatusUpdateSerial(u32);
140
141impl StatusUpdateSerial {
142 pub fn new(id: u32) -> StatusUpdateSerial {
144 StatusUpdateSerial(id)
145 }
146
147 pub const MIN: Self = Self(1);
149 pub const MAX: Self = Self(u32::MAX - 1);
151
152 pub fn to_u32(self) -> u32 {
155 self.0
156 }
157}
158
159impl rusqlite::types::ToSql for StatusUpdateSerial {
160 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
161 let val = rusqlite::types::Value::Integer(i64::from(self.0));
162 let out = rusqlite::types::ToSqlOutput::Owned(val);
163 Ok(out)
164 }
165}
166
167#[derive(Debug, Deserialize)]
169struct StatusUpdates {
170 updates: Vec<StatusUpdateItem>,
171}
172
173#[derive(Debug, Serialize, Deserialize, Default)]
175pub struct StatusUpdateItem {
176 pub payload: Value,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
182 pub info: Option<String>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
187 pub href: Option<String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
192 pub document: Option<String>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
198 pub summary: Option<String>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
205 pub uid: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub notify: Option<HashMap<String, String>>,
210}
211
212#[derive(Debug, Serialize, Deserialize)]
214pub(crate) struct StatusUpdateItemAndSerial {
215 #[serde(flatten)]
216 item: StatusUpdateItem,
217
218 serial: StatusUpdateSerial,
219 max_serial: StatusUpdateSerial,
220}
221
222fn find_zip_entry<'a>(
224 file: &'a async_zip::ZipFile,
225 name: &str,
226) -> Option<(usize, &'a async_zip::StoredZipEntry)> {
227 for (i, ent) in file.entries().iter().enumerate() {
228 if ent.filename().as_bytes() == name.as_bytes() {
229 return Some((i, ent));
230 }
231 }
232 None
233}
234
235const STATUS_UPDATE_SIZE_MAX: usize = 100 << 10;
237
238impl Context {
239 pub(crate) async fn is_webxdc_file(&self, filename: &str, file: &[u8]) -> Result<bool> {
241 if !filename.ends_with(WEBXDC_SUFFIX) {
242 return Ok(false);
243 }
244
245 let archive = match async_zip::base::read::mem::ZipFileReader::new(file.to_vec()).await {
246 Ok(archive) => archive,
247 Err(_) => {
248 info!(self, "{} cannot be opened as zip-file", &filename);
249 return Ok(false);
250 }
251 };
252
253 if find_zip_entry(archive.file(), "index.html").is_none() {
254 info!(self, "{} misses index.html", &filename);
255 return Ok(false);
256 }
257
258 Ok(true)
259 }
260
261 pub(crate) async fn ensure_sendable_webxdc_file(&self, path: &Path) -> Result<()> {
263 let filename = path.to_str().unwrap_or_default();
264
265 let file = BufReader::new(File::open(path).await?);
266 let valid = match SeekZipFileReader::with_tokio(file).await {
267 Ok(archive) => {
268 if find_zip_entry(archive.file(), "index.html").is_none() {
269 warn!(self, "{} misses index.html", filename);
270 false
271 } else {
272 true
273 }
274 }
275 Err(_) => {
276 warn!(self, "{} cannot be opened as zip-file", filename);
277 false
278 }
279 };
280
281 if !valid {
282 bail!("{filename} is not a valid webxdc file");
283 }
284
285 Ok(())
286 }
287
288 async fn get_overwritable_info_msg_id(
291 &self,
292 instance: &Message,
293 from_id: ContactId,
294 ) -> Result<Option<MsgId>> {
295 if let Some((last_msg_id, last_from_id, last_param, last_in_repl_to)) = self
296 .sql
297 .query_row_optional(
298 r#"SELECT id, from_id, param, mime_in_reply_to
299 FROM msgs
300 WHERE chat_id=?1 AND hidden=0
301 ORDER BY timestamp DESC, id DESC LIMIT 1"#,
302 (instance.chat_id,),
303 |row| {
304 let last_msg_id: MsgId = row.get(0)?;
305 let last_from_id: ContactId = row.get(1)?;
306 let last_param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
307 let last_in_repl_to: String = row.get(3)?;
308 Ok((last_msg_id, last_from_id, last_param, last_in_repl_to))
309 },
310 )
311 .await?
312 && last_from_id == from_id
313 && last_param.get_cmd() == SystemMessage::WebxdcInfoMessage
314 && last_in_repl_to == instance.rfc724_mid
315 {
316 return Ok(Some(last_msg_id));
317 }
318 Ok(None)
319 }
320
321 async fn create_status_update_record(
324 &self,
325 instance: &Message,
326 status_update_item: StatusUpdateItem,
327 timestamp: i64,
328 can_info_msg: bool,
329 from_id: ContactId,
330 ) -> Result<Option<StatusUpdateSerial>> {
331 let Some(status_update_serial) = self
332 .write_status_update_inner(&instance.id, &status_update_item, timestamp)
333 .await?
334 else {
335 return Ok(None);
336 };
337
338 let mut notify_msg_id = instance.id;
339 let mut param_changed = false;
340
341 let mut instance = instance.clone();
342 if let Some(ref document) = status_update_item.document
343 && instance
344 .param
345 .update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
346 {
347 instance.param.set(Param::WebxdcDocument, document);
348 param_changed = true;
349 }
350
351 if let Some(ref summary) = status_update_item.summary
352 && instance
353 .param
354 .update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
355 {
356 let summary = sanitize_bidi_characters(summary);
357 instance.param.set(Param::WebxdcSummary, summary.clone());
358 param_changed = true;
359 }
360
361 if can_info_msg && let Some(ref info) = status_update_item.info {
362 let info_msg_id = self
363 .get_overwritable_info_msg_id(&instance, from_id)
364 .await?;
365
366 if let (Some(info_msg_id), None) = (info_msg_id, &status_update_item.href) {
367 chat::update_msg_text_and_timestamp(
368 self,
369 instance.chat_id,
370 info_msg_id,
371 info.as_str(),
372 timestamp,
373 )
374 .await?;
375 notify_msg_id = info_msg_id;
376 } else {
377 notify_msg_id = chat::add_info_msg_with_cmd(
378 self,
379 instance.chat_id,
380 info.as_str(),
381 SystemMessage::WebxdcInfoMessage,
382 Some(timestamp),
383 timestamp,
384 Some(&instance),
385 Some(from_id),
386 None,
387 )
388 .await?;
389 }
390
391 if let Some(ref href) = status_update_item.href {
392 let mut notify_msg = Message::load_from_db(self, notify_msg_id)
393 .await
394 .context("Failed to load just created notification message")?;
395 notify_msg.param.set(Param::Arg, href);
396 notify_msg.update_param(self).await?;
397 }
398 }
399
400 if param_changed {
401 instance.update_param(self).await?;
402 self.emit_msgs_changed(instance.chat_id, instance.id);
403 }
404
405 if instance.viewtype == Viewtype::Webxdc {
406 self.emit_event(EventType::WebxdcStatusUpdate {
407 msg_id: instance.id,
408 status_update_serial,
409 });
410 }
411
412 if from_id != ContactId::SELF
413 && let Some(notify_list) = status_update_item.notify
414 {
415 let self_addr = instance.get_webxdc_self_addr(self).await?;
416 let notify_text = if let Some(notify_text) = notify_list.get(&self_addr) {
417 Some(notify_text)
418 } else if let Some(notify_text) = notify_list.get("*")
419 && !Chat::load_from_db(self, instance.chat_id).await?.is_muted()
420 {
421 Some(notify_text)
422 } else {
423 None
424 };
425 if let Some(notify_text) = notify_text {
426 self.emit_event(EventType::IncomingWebxdcNotify {
427 chat_id: instance.chat_id,
428 contact_id: from_id,
429 msg_id: notify_msg_id,
430 text: notify_text.clone(),
431 href: status_update_item.href,
432 });
433 }
434 }
435
436 Ok(Some(status_update_serial))
437 }
438
439 pub(crate) async fn write_status_update_inner(
443 &self,
444 instance_id: &MsgId,
445 status_update_item: &StatusUpdateItem,
446 timestamp: i64,
447 ) -> Result<Option<StatusUpdateSerial>> {
448 let uid = status_update_item.uid.as_deref();
449 let status_update_item = serde_json::to_string(&status_update_item)?;
450 let trans_fn = |t: &mut rusqlite::Transaction| {
451 t.execute(
452 "UPDATE msgs SET timestamp_rcvd=? WHERE id=?",
453 (timestamp, instance_id),
454 )?;
455 let rowid = t
456 .query_row(
457 "INSERT INTO msgs_status_updates (msg_id, update_item, uid) VALUES(?, ?, ?)
458 ON CONFLICT (uid) DO NOTHING
459 RETURNING id",
460 (instance_id, status_update_item, uid),
461 |row| {
462 let id: u32 = row.get(0)?;
463 Ok(id)
464 },
465 )
466 .optional()?;
467 Ok(rowid)
468 };
469 let Some(rowid) = self.sql.transaction(trans_fn).await? else {
470 let uid = uid.unwrap_or("-");
471 info!(self, "Ignoring duplicate status update with uid={uid}");
472 return Ok(None);
473 };
474 let status_update_serial = StatusUpdateSerial(rowid);
475 Ok(Some(status_update_serial))
476 }
477
478 pub async fn get_status_update(
480 &self,
481 msg_id: MsgId,
482 status_update_serial: StatusUpdateSerial,
483 ) -> Result<String> {
484 self.sql
485 .query_get_value(
486 "SELECT update_item FROM msgs_status_updates WHERE id=? AND msg_id=? ",
487 (status_update_serial.0, msg_id),
488 )
489 .await?
490 .context("get_status_update: no update item found.")
491 }
492
493 pub async fn send_webxdc_status_update(
499 &self,
500 instance_msg_id: MsgId,
501 update_str: &str,
502 ) -> Result<()> {
503 let status_update_item: StatusUpdateItem = serde_json::from_str(update_str)
504 .with_context(|| format!("Failed to parse webxdc update item from {update_str:?}"))?;
505 self.send_webxdc_status_update_struct(instance_msg_id, status_update_item)
506 .await?;
507 Ok(())
508 }
509
510 pub async fn send_webxdc_status_update_struct(
513 &self,
514 instance_msg_id: MsgId,
515 mut status_update: StatusUpdateItem,
516 ) -> Result<()> {
517 let instance = Message::load_from_db(self, instance_msg_id)
518 .await
519 .with_context(|| {
520 format!("Failed to load message {instance_msg_id} from the database")
521 })?;
522 let viewtype = instance.viewtype;
523 if viewtype != Viewtype::Webxdc {
524 bail!(
525 "send_webxdc_status_update: message {instance_msg_id} is not a webxdc message, but a {viewtype} message."
526 );
527 }
528
529 if instance.param.get_int(Param::WebxdcIntegration).is_some() {
530 return self
531 .intercept_send_webxdc_status_update(instance, status_update)
532 .await;
533 }
534
535 let chat_id = instance.chat_id;
536 let chat = Chat::load_from_db(self, chat_id)
537 .await
538 .with_context(|| format!("Failed to load chat {chat_id} from the database"))?;
539 if let Some(reason) = chat.why_cant_send(self).await.with_context(|| {
540 format!("Failed to check if webxdc update can be sent to chat {chat_id}")
541 })? {
542 bail!("Cannot send to {chat_id}: {reason}.");
543 }
544
545 let send_now = !matches!(
546 instance.state,
547 MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft
548 );
549
550 status_update.uid = Some(create_id());
551 let status_update_serial: StatusUpdateSerial = self
552 .create_status_update_record(
553 &instance,
554 status_update,
555 create_smeared_timestamp(self),
556 send_now,
557 ContactId::SELF,
558 )
559 .await
560 .context("Failed to create status update")?
561 .context("Duplicate status update UID was generated")?;
562
563 if send_now {
564 self.sql.insert(
565 "INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) VALUES(?, ?, ?, '')
566 ON CONFLICT(msg_id)
567 DO UPDATE SET last_serial=excluded.last_serial",
568 (instance.id, status_update_serial, status_update_serial),
569 ).await.context("Failed to insert webxdc update into SMTP queue")?;
570 self.scheduler.interrupt_smtp().await;
571 }
572 Ok(())
573 }
574
575 async fn smtp_status_update_get(&self) -> Result<Option<(MsgId, i64, StatusUpdateSerial)>> {
577 let res = self
578 .sql
579 .query_row_optional(
580 "SELECT msg_id, first_serial, last_serial \
581 FROM smtp_status_updates LIMIT 1",
582 (),
583 |row| {
584 let instance_id: MsgId = row.get(0)?;
585 let first_serial: i64 = row.get(1)?;
586 let last_serial: StatusUpdateSerial = row.get(2)?;
587 Ok((instance_id, first_serial, last_serial))
588 },
589 )
590 .await?;
591 Ok(res)
592 }
593
594 async fn smtp_status_update_pop_serials(
595 &self,
596 msg_id: MsgId,
597 first: i64,
598 first_new: StatusUpdateSerial,
599 ) -> Result<()> {
600 if self
601 .sql
602 .execute(
603 "DELETE FROM smtp_status_updates \
604 WHERE msg_id=? AND first_serial=? AND last_serial<?",
605 (msg_id, first, first_new),
606 )
607 .await?
608 > 0
609 {
610 return Ok(());
611 }
612 self.sql
613 .execute(
614 "UPDATE smtp_status_updates SET first_serial=? \
615 WHERE msg_id=? AND first_serial=?",
616 (first_new, msg_id, first),
617 )
618 .await?;
619 Ok(())
620 }
621
622 pub(crate) async fn flush_status_updates(&self) -> Result<()> {
624 loop {
625 let (instance_id, first, last) = match self.smtp_status_update_get().await? {
626 Some(res) => res,
627 None => return Ok(()),
628 };
629 let (json, first_new) = self
630 .render_webxdc_status_update_object(
631 instance_id,
632 StatusUpdateSerial(max(first, 1).try_into()?),
633 last,
634 Some(STATUS_UPDATE_SIZE_MAX),
635 )
636 .await?;
637 if let Some(json) = json {
638 let instance = Message::load_from_db(self, instance_id).await?;
639 let mut status_update = Message {
640 chat_id: instance.chat_id,
641 viewtype: Viewtype::Text,
642 text: BODY_DESCR.to_string(),
643 hidden: true,
644 ..Default::default()
645 };
646 status_update
647 .param
648 .set_cmd(SystemMessage::WebxdcStatusUpdate);
649 status_update.param.set(Param::Arg, json);
650 status_update.set_quote(self, Some(&instance)).await?;
651 status_update.param.remove(Param::GuaranteeE2ee); chat::send_msg(self, instance.chat_id, &mut status_update).await?;
653 }
654 self.smtp_status_update_pop_serials(instance_id, first, first_new)
655 .await?;
656 }
657 }
658
659 pub(crate) fn build_status_update_part(&self, json: &str) -> MimePart<'static> {
660 MimePart::new("application/json", json.as_bytes().to_vec()).attachment("status-update.json")
661 }
662
663 pub(crate) async fn receive_status_update(
675 &self,
676 from_id: ContactId,
677 instance: &Message,
678 timestamp: i64,
679 can_info_msg: bool,
680 json: &str,
681 ) -> Result<()> {
682 let chat_id = instance.chat_id;
683
684 if from_id != ContactId::SELF && !chat::is_contact_in_chat(self, chat_id, from_id).await? {
685 let chat_type: Chattype = self
686 .sql
687 .query_get_value("SELECT type FROM chats WHERE id=?", (chat_id,))
688 .await?
689 .with_context(|| format!("Chat type for chat {chat_id} not found"))?;
690 if chat_type != Chattype::Mailinglist {
691 bail!(
692 "receive_status_update: status sender {from_id} is not a member of chat {chat_id}"
693 )
694 }
695 }
696
697 let updates: StatusUpdates = serde_json::from_str(json)?;
698 for update_item in updates.updates {
699 self.create_status_update_record(
700 instance,
701 update_item,
702 timestamp,
703 can_info_msg,
704 from_id,
705 )
706 .await?;
707 }
708
709 Ok(())
710 }
711
712 pub async fn get_webxdc_status_updates(
720 &self,
721 instance_msg_id: MsgId,
722 last_known_serial: StatusUpdateSerial,
723 ) -> Result<String> {
724 let param = instance_msg_id.get_param(self).await?;
725 if param.get_int(Param::WebxdcIntegration).is_some() {
726 let instance = Message::load_from_db(self, instance_msg_id).await?;
727 return self
728 .intercept_get_webxdc_status_updates(instance, last_known_serial)
729 .await;
730 }
731
732 let json = self
733 .sql
734 .query_map(
735 "SELECT update_item, id FROM msgs_status_updates WHERE msg_id=? AND id>? ORDER BY id",
736 (instance_msg_id, last_known_serial),
737 |row| {
738 let update_item_str: String = row.get(0)?;
739 let serial: StatusUpdateSerial = row.get(1)?;
740 Ok((update_item_str, serial))
741 },
742 |rows| {
743 let mut rows_copy : Vec<(String, StatusUpdateSerial)> = Vec::new(); let mut max_serial = StatusUpdateSerial(0);
745 for row in rows {
746 let row = row?;
747 if row.1 > max_serial {
748 max_serial = row.1;
749 }
750 rows_copy.push(row);
751 }
752
753 let mut json = String::default();
754 for row in rows_copy {
755 let (update_item_str, serial) = row;
756 let update_item = StatusUpdateItemAndSerial
757 {
758 item: StatusUpdateItem {
759 uid: None, ..serde_json::from_str(&update_item_str)?
761 },
762 serial,
763 max_serial,
764 };
765
766 if !json.is_empty() {
767 json.push_str(",\n");
768 }
769 json.push_str(&serde_json::to_string(&update_item)?);
770 }
771 Ok(json)
772 },
773 )
774 .await?;
775 Ok(format!("[{json}]"))
776 }
777
778 #[expect(clippy::arithmetic_side_effects)]
788 pub(crate) async fn render_webxdc_status_update_object(
789 &self,
790 instance_msg_id: MsgId,
791 first: StatusUpdateSerial,
792 last: StatusUpdateSerial,
793 size_max: Option<usize>,
794 ) -> Result<(Option<String>, StatusUpdateSerial)> {
795 let (json, first_new) = self
796 .sql
797 .query_map(
798 "SELECT id, update_item FROM msgs_status_updates \
799 WHERE msg_id=? AND id>=? AND id<=? ORDER BY id",
800 (instance_msg_id, first, last),
801 |row| {
802 let id: StatusUpdateSerial = row.get(0)?;
803 let update_item: String = row.get(1)?;
804 Ok((id, update_item))
805 },
806 |rows| {
807 let mut json = String::default();
808 for row in rows {
809 let (id, update_item) = row?;
810 if !json.is_empty()
811 && json.len() + update_item.len() >= size_max.unwrap_or(usize::MAX)
812 {
813 return Ok((json, id));
814 }
815 if !json.is_empty() {
816 json.push_str(",\n");
817 }
818 json.push_str(&update_item);
819 }
820 Ok((
821 json,
822 StatusUpdateSerial::new(last.to_u32().saturating_add(1)),
825 ))
826 },
827 )
828 .await?;
829 let json = match json.is_empty() {
830 true => None,
831 false => Some(format!(r#"{{"updates":[{json}]}}"#)),
832 };
833 Ok((json, first_new))
834 }
835}
836
837fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
838 let s = std::str::from_utf8(bytes)?;
839 let manifest: WebxdcManifest = toml::from_str(s)?;
840 Ok(manifest)
841}
842
843async fn get_blob(archive: &mut SeekZipFileReader<BufReader<File>>, name: &str) -> Result<Vec<u8>> {
844 let (i, _) =
845 find_zip_entry(archive.file(), name).ok_or_else(|| anyhow!("no entry found for {name}"))?;
846 let mut reader = archive.reader_with_entry(i).await?;
847 let mut buf = Vec::new();
848 reader.read_to_end_checked(&mut buf).await?;
849 Ok(buf)
850}
851
852impl Message {
853 async fn get_webxdc_archive(
856 &self,
857 context: &Context,
858 ) -> Result<SeekZipFileReader<BufReader<File>>> {
859 let path = self
860 .get_file(context)
861 .ok_or_else(|| format_err!("No webxdc instance file."))?;
862 let path_abs = get_abs_path(context, &path);
863 let file = BufReader::new(File::open(path_abs).await?);
864 let archive = SeekZipFileReader::with_tokio(file).await?;
865 Ok(archive)
866 }
867
868 pub async fn get_webxdc_blob(&self, context: &Context, name: &str) -> Result<Vec<u8>> {
873 ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
874
875 if name == WEBXDC_DEFAULT_ICON {
876 return Ok(include_bytes!("../assets/icon-webxdc.png").to_vec());
877 }
878
879 let name = if name.starts_with('/') {
882 name.split_at(1).1
883 } else {
884 name
885 };
886
887 let mut archive = self.get_webxdc_archive(context).await?;
888
889 if name == "index.html"
890 && let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await
891 && let Ok(manifest) = parse_webxdc_manifest(&bytes)
892 && let Some(min_api) = manifest.min_api
893 && min_api > WEBXDC_API_VERSION
894 {
895 return Ok(Vec::from(
896 "<!DOCTYPE html>This Webxdc requires a newer Delta Chat version.",
897 ));
898 }
899
900 get_blob(&mut archive, name).await
901 }
902
903 pub async fn get_webxdc_info(&self, context: &Context) -> Result<WebxdcInfo> {
905 ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
906 let mut archive = self.get_webxdc_archive(context).await?;
907
908 let mut manifest = get_blob(&mut archive, "manifest.toml")
909 .await
910 .map(|bytes| parse_webxdc_manifest(&bytes).unwrap_or_default())
911 .unwrap_or_default();
912
913 if let Some(ref name) = manifest.name {
914 let name = name.trim();
915 if name.is_empty() {
916 warn!(context, "empty name given in manifest");
917 manifest.name = None;
918 }
919 }
920
921 let request_integration = manifest.request_integration.unwrap_or_default();
922 let is_integrated = self.is_set_as_webxdc_integration(context).await?;
923 let internet_access = is_integrated;
924
925 let self_addr = self.get_webxdc_self_addr(context).await?;
926
927 Ok(WebxdcInfo {
928 name: if let Some(name) = manifest.name {
929 name
930 } else {
931 self.get_filename().unwrap_or_default()
932 },
933 icon: if find_zip_entry(archive.file(), "icon.png").is_some() {
934 "icon.png".to_string()
935 } else if find_zip_entry(archive.file(), "icon.jpg").is_some() {
936 "icon.jpg".to_string()
937 } else {
938 WEBXDC_DEFAULT_ICON.to_string()
939 },
940 document: self
941 .param
942 .get(Param::WebxdcDocument)
943 .unwrap_or_default()
944 .to_string(),
945 summary: if is_integrated {
946 "🌍 Used as map. Delete to use default. Do not enter sensitive data".to_string()
947 } else if request_integration == "map" {
948 "🌏 To use as map, forward to \"Saved Messages\" again. Do not enter sensitive data"
949 .to_string()
950 } else {
951 self.param
952 .get(Param::WebxdcSummary)
953 .unwrap_or_default()
954 .to_string()
955 },
956 source_code_url: if let Some(url) = manifest.source_code_url {
957 url
958 } else {
959 "".to_string()
960 },
961 request_integration,
962 internet_access,
963 self_addr,
964 send_update_interval: context.ratelimit.read().await.update_interval(),
965 send_update_max_size: RECOMMENDED_FILE_SIZE as usize,
966 })
967 }
968
969 async fn get_webxdc_self_addr(&self, context: &Context) -> Result<String> {
970 let fingerprint = self_fingerprint(context).await?;
971 let data = format!("{}-{}", fingerprint, self.rfc724_mid);
972 let hash = Sha256::digest(data.as_bytes());
973 Ok(format!("{hash:x}"))
974 }
975
976 pub fn get_webxdc_href(&self) -> Option<String> {
982 self.param.get(Param::Arg).map(|href| href.to_string())
983 }
984}
985
986#[cfg(test)]
987mod webxdc_tests;