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