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    /// Define if the local user is the one who initially shared the webxdc application in the chat.
115    pub is_app_sender: bool,
116
117    /// Define if the app runs in a broadcasting context.
118    pub is_broadcast: bool,
119
120    /// Milliseconds to wait before calling `sendUpdate()` again since the last call.
121    /// Should be exposed to `window.sendUpdateInterval` in JS land.
122    pub send_update_interval: usize,
123
124    /// Maximum number of bytes accepted for a serialized update object.
125    /// Should be exposed to `window.sendUpdateMaxSize` in JS land.
126    pub send_update_max_size: usize,
127}
128
129/// Status Update ID.
130#[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    /// Create a new [StatusUpdateSerial].
149    pub fn new(id: u32) -> StatusUpdateSerial {
150        StatusUpdateSerial(id)
151    }
152
153    /// Minimum value.
154    pub const MIN: Self = Self(1);
155    /// Maximum value.
156    pub const MAX: Self = Self(u32::MAX - 1);
157
158    /// Gets StatusUpdateSerial as untyped integer.
159    /// Avoid using this outside ffi.
160    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// Array of update items as sent on the wire.
174#[derive(Debug, Deserialize)]
175struct StatusUpdates {
176    updates: Vec<StatusUpdateItem>,
177}
178
179/// Update items as sent on the wire and as stored in the database.
180#[derive(Debug, Serialize, Deserialize, Default)]
181pub struct StatusUpdateItem {
182    /// The playload of the status update.
183    pub payload: Value,
184
185    /// Optional short info message that will be displayed in the chat.
186    /// For example "Alice added an item" or "Bob voted for option x".
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub info: Option<String>,
189
190    /// Optional link the info message will point to.
191    /// Used to set `window.location.href` in JS land.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub href: Option<String>,
194
195    /// The new name of the editing document.
196    /// This is not needed if the webxdc doesn't edit documents.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub document: Option<String>,
199
200    /// Optional summary of the status update which will be shown next to the
201    /// app icon. This should be short and can be something like "8 votes"
202    /// for a voting app.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub summary: Option<String>,
205
206    /// Unique ID for deduplication.
207    /// This can be used if the message is sent over multiple transports.
208    ///
209    /// If there is no ID, message is always considered to be unique.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub uid: Option<String>,
212
213    /// Array of other users `selfAddr` that should be notified about this update.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub notify: Option<HashMap<String, String>>,
216}
217
218/// Update items as passed to the UIs.
219#[derive(Debug, Serialize, Deserialize)]
220pub(crate) struct StatusUpdateItemAndSerial {
221    #[serde(flatten)]
222    item: StatusUpdateItem,
223
224    serial: StatusUpdateSerial,
225    max_serial: StatusUpdateSerial,
226}
227
228/// Returns an entry index and a reference.
229fn 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
241/// Status update JSON size soft limit.
242const STATUS_UPDATE_SIZE_MAX: usize = 100 << 10;
243
244impl Context {
245    /// check if a file is an acceptable webxdc for sending or receiving.
246    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    /// Ensure that a file is an acceptable webxdc for sending.
268    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    /// Check if the last message of a chat is an info message belonging to the given instance and sender.
295    /// If so, the id of this message is returned.
296    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    /// Takes an update-json as `{payload: PAYLOAD}`
328    /// writes it to the database and handles events, info-messages, document name and summary.
329    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    /// Inserts a status update item into `msgs_status_updates` table.
446    ///
447    /// Returns serial ID of the status update if a new item is inserted.
448    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    /// Returns the update_item with `status_update_serial` from the webxdc with message id `msg_id`.
485    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    /// Sends a status update for an webxdc instance.
500    ///
501    /// If the instance is a draft,
502    /// the status update is sent once the instance is actually sent.
503    /// Otherwise, the update is sent as soon as possible.
504    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    /// Sends a status update for an webxdc instance.
517    /// Also see [Self::send_webxdc_status_update]
518    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    /// Returns one record of the queued webxdc status updates.
582    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    /// Attempts to send queued webxdc status updates.
629    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); // may be set by set_quote(), if #2985 is done, this line can be removed
658                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    /// Receives status updates from receive_imf to the database
670    /// and sends out an event.
671    ///
672    /// `instance` is a webxdc instance.
673    ///
674    /// `from_id` is the sender.
675    ///
676    /// `timestamp` is the timestamp of the update.
677    ///
678    /// `json` is an array containing one or more update items as created by send_webxdc_status_update(),
679    /// the array is parsed using serde, the single payloads are used as is.
680    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    /// Returns status updates as an JSON-array, ready to be consumed by a webxdc.
719    ///
720    /// Example: `[{"serial":1, "max_serial":3, "payload":"any update data"},
721    ///            {"serial":3, "max_serial":3, "payload":"another update data"}]`
722    /// Updates with serials larger than `last_known_serial` are returned.
723    /// If no last serial is known, set `last_known_serial` to 0.
724    /// If no updates are available, an empty JSON-array is returned.
725    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(); // `rows_copy` needed as `rows` cannot be iterated twice.
750                    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, // Erase UIDs, apps, bots and tests don't need to know them.
766                                ..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    /// Renders JSON-object for status updates as used on the wire.
785    ///
786    /// Returns optional JSON and the first serial of updates not included due to a JSON size
787    /// limit. If all requested updates are included, returns the first not requested serial.
788    ///
789    /// Example JSON: `{"updates": [{"payload":"any update data"},
790    ///                             {"payload":"another update data"}]}`
791    ///
792    /// * `(first, last)`: range of status update serials to send.
793    #[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                        // Too late to fail here if an overflow happens. It's still better to send
829                        // the updates.
830                        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    /// Get handle to a webxdc ZIP-archive.
860    /// To check for file existence use archive.by_name(), to read a file, use get_blob(archive).
861    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    /// Return file from inside an archive.
875    /// Currently, this works only if the message is an webxdc instance.
876    ///
877    /// `name` is the filename within the archive, e.g. `index.html`.
878    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        // ignore first slash.
886        // this way, files can be accessed absolutely (`/index.html`) as well as relatively (`index.html`)
887        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    /// Return info from manifest.toml or from fallbacks.
910    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    /// Get link attached to an info message.
990    ///
991    /// The info message needs to be of type SystemMessage::WebxdcInfoMessage.
992    /// Typically, this is used to start the corresponding webxdc app
993    /// with `window.location.href` set in JS land.
994    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;