deltachat/
webxdc.rs

1//! # Handle webxdc messages.
2//!
3//! Internally status updates are stored in the `msgs_status_updates` SQL table.
4//! `msgs_status_updates` contains the following columns:
5//! - `id` - status update serial number
6//! - `msg_id` - ID of the message in the `msgs` table
7//! - `update_item` - JSON representation of the status update
8//! - `uid` - "id" field of the update, used for deduplication
9//!
10//! Status updates are scheduled for sending by adding a record
11//! to `smtp_status_updates_table` SQL table.
12//! `smtp_status_updates` contains the following columns:
13//! - `msg_id` - ID of the message in the `msgs` table
14//! - `first_serial` - serial number of the first status update to send
15//! - `last_serial` - serial number of the last status update to send
16//! - `descr` - not used, set to empty string
17
18mod 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
51/// The current API version.
52/// If `min_api` in manifest.toml is set to a larger value,
53/// the Webxdc's index.html is replaced by an error message.
54/// In the future, that may be useful to avoid new Webxdc being loaded on old Delta Chats.
55const WEBXDC_API_VERSION: u32 = 1;
56
57/// Suffix used to recognize webxdc files.
58pub const WEBXDC_SUFFIX: &str = "xdc";
59const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
60
61/// Text shown to classic e-mail users in the visible e-mail body.
62const BODY_DESCR: &str = "Webxdc Status Update";
63
64/// Raw information read from manifest.toml
65#[derive(Debug, Deserialize, Default)]
66#[non_exhaustive]
67pub struct WebxdcManifest {
68    /// Webxdc name, used on icons or page titles.
69    pub name: Option<String>,
70
71    /// Minimum API version required to run this webxdc.
72    pub min_api: Option<u32>,
73
74    /// Optional URL of webxdc source code.
75    pub source_code_url: Option<String>,
76
77    /// Set to "map" to request integration.
78    pub request_integration: Option<String>,
79}
80
81/// Parsed information from WebxdcManifest and fallbacks.
82#[derive(Debug, Serialize)]
83pub struct WebxdcInfo {
84    /// The name of the app.
85    /// Defaults to filename if not set in the manifest.
86    pub name: String,
87
88    /// Filename of the app icon.
89    pub icon: String,
90
91    /// If the webxdc represents a document and allows to edit it,
92    /// this is the document name.
93    /// Otherwise an empty string.
94    pub document: String,
95
96    /// Short description of the webxdc state.
97    /// For example, "7 votes".
98    pub summary: String,
99
100    /// URL of webxdc source code or an empty string.
101    pub source_code_url: String,
102
103    /// Set to "map" to request integration, otherwise an empty string.
104    pub request_integration: String,
105
106    /// If the webxdc is allowed to access the network.
107    /// It should request access, be encrypted
108    /// and sent to self for this.
109    pub internet_access: bool,
110
111    /// Address to be used for `window.webxdc.selfAddr` in JS land.
112    pub self_addr: String,
113
114    /// Milliseconds to wait before calling `sendUpdate()` again since the last call.
115    /// Should be exposed to `window.sendUpdateInterval` in JS land.
116    pub send_update_interval: usize,
117
118    /// Maximum number of bytes accepted for a serialized update object.
119    /// Should be exposed to `window.sendUpdateMaxSize` in JS land.
120    pub send_update_max_size: usize,
121}
122
123/// Status Update ID.
124#[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    /// Create a new [StatusUpdateSerial].
143    pub fn new(id: u32) -> StatusUpdateSerial {
144        StatusUpdateSerial(id)
145    }
146
147    /// Minimum value.
148    pub const MIN: Self = Self(1);
149    /// Maximum value.
150    pub const MAX: Self = Self(u32::MAX - 1);
151
152    /// Gets StatusUpdateSerial as untyped integer.
153    /// Avoid using this outside ffi.
154    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// Array of update items as sent on the wire.
168#[derive(Debug, Deserialize)]
169struct StatusUpdates {
170    updates: Vec<StatusUpdateItem>,
171}
172
173/// Update items as sent on the wire and as stored in the database.
174#[derive(Debug, Serialize, Deserialize, Default)]
175pub struct StatusUpdateItem {
176    /// The playload of the status update.
177    pub payload: Value,
178
179    /// Optional short info message that will be displayed in the chat.
180    /// For example "Alice added an item" or "Bob voted for option x".
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub info: Option<String>,
183
184    /// Optional link the info message will point to.
185    /// Used to set `window.location.href` in JS land.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub href: Option<String>,
188
189    /// The new name of the editing document.
190    /// This is not needed if the webxdc doesn't edit documents.
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub document: Option<String>,
193
194    /// Optional summary of the status update which will be shown next to the
195    /// app icon. This should be short and can be something like "8 votes"
196    /// for a voting app.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub summary: Option<String>,
199
200    /// Unique ID for deduplication.
201    /// This can be used if the message is sent over multiple transports.
202    ///
203    /// If there is no ID, message is always considered to be unique.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub uid: Option<String>,
206
207    /// Array of other users `selfAddr` that should be notified about this update.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub notify: Option<HashMap<String, String>>,
210}
211
212/// Update items as passed to the UIs.
213#[derive(Debug, Serialize, Deserialize)]
214pub(crate) struct StatusUpdateItemAndSerial {
215    #[serde(flatten)]
216    item: StatusUpdateItem,
217
218    serial: StatusUpdateSerial,
219    max_serial: StatusUpdateSerial,
220}
221
222/// Returns an entry index and a reference.
223fn 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
235/// Status update JSON size soft limit.
236const STATUS_UPDATE_SIZE_MAX: usize = 100 << 10;
237
238impl Context {
239    /// check if a file is an acceptable webxdc for sending or receiving.
240    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    /// Ensure that a file is an acceptable webxdc for sending.
262    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    /// Check if the last message of a chat is an info message belonging to the given instance and sender.
289    /// If so, the id of this message is returned.
290    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    /// Takes an update-json as `{payload: PAYLOAD}`
322    /// writes it to the database and handles events, info-messages, document name and summary.
323    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    /// Inserts a status update item into `msgs_status_updates` table.
440    ///
441    /// Returns serial ID of the status update if a new item is inserted.
442    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    /// Returns the update_item with `status_update_serial` from the webxdc with message id `msg_id`.
479    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    /// Sends a status update for an webxdc instance.
494    ///
495    /// If the instance is a draft,
496    /// the status update is sent once the instance is actually sent.
497    /// Otherwise, the update is sent as soon as possible.
498    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    /// Sends a status update for an webxdc instance.
511    /// Also see [Self::send_webxdc_status_update]
512    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    /// Returns one record of the queued webxdc status updates.
576    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    /// Attempts to send queued webxdc status updates.
623    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); // may be set by set_quote(), if #2985 is done, this line can be removed
652                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    /// Receives status updates from receive_imf to the database
664    /// and sends out an event.
665    ///
666    /// `instance` is a webxdc instance.
667    ///
668    /// `from_id` is the sender.
669    ///
670    /// `timestamp` is the timestamp of the update.
671    ///
672    /// `json` is an array containing one or more update items as created by send_webxdc_status_update(),
673    /// the array is parsed using serde, the single payloads are used as is.
674    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    /// Returns status updates as an JSON-array, ready to be consumed by a webxdc.
713    ///
714    /// Example: `[{"serial":1, "max_serial":3, "payload":"any update data"},
715    ///            {"serial":3, "max_serial":3, "payload":"another update data"}]`
716    /// Updates with serials larger than `last_known_serial` are returned.
717    /// If no last serial is known, set `last_known_serial` to 0.
718    /// If no updates are available, an empty JSON-array is returned.
719    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(); // `rows_copy` needed as `rows` cannot be iterated twice.
744                    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, // Erase UIDs, apps, bots and tests don't need to know them.
760                                ..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    /// Renders JSON-object for status updates as used on the wire.
779    ///
780    /// Returns optional JSON and the first serial of updates not included due to a JSON size
781    /// limit. If all requested updates are included, returns the first not requested serial.
782    ///
783    /// Example JSON: `{"updates": [{"payload":"any update data"},
784    ///                             {"payload":"another update data"}]}`
785    ///
786    /// * `(first, last)`: range of status update serials to send.
787    #[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                        // Too late to fail here if an overflow happens. It's still better to send
823                        // the updates.
824                        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    /// Get handle to a webxdc ZIP-archive.
854    /// To check for file existence use archive.by_name(), to read a file, use get_blob(archive).
855    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    /// Return file from inside an archive.
869    /// Currently, this works only if the message is an webxdc instance.
870    ///
871    /// `name` is the filename within the archive, e.g. `index.html`.
872    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        // ignore first slash.
880        // this way, files can be accessed absolutely (`/index.html`) as well as relatively (`index.html`)
881        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    /// Return info from manifest.toml or from fallbacks.
904    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    /// Get link attached to an info message.
977    ///
978    /// The info message needs to be of type SystemMessage::WebxdcInfoMessage.
979    /// Typically, this is used to start the corresponding webxdc app
980    /// with `window.location.href` set in JS land.
981    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;