mirror of
https://github.com/chatmail/core.git
synced 2026-04-19 14:36:29 +03:00
It's a more stable interface, if `shift()` suddenly will need to wait for smth like finalisation of parallel running critical sections assuming there are no time jumps, we don't need to fix all its usages.
2701 lines
100 KiB
Rust
2701 lines
100 KiB
Rust
//! # Handle webxdc messages.
|
|
//!
|
|
//! Internally status updates are stored in the `msgs_status_updates` SQL table.
|
|
//! `msgs_status_updates` contains the following columns:
|
|
//! - `id` - status update serial number
|
|
//! - `msg_id` - ID of the message in the `msgs` table
|
|
//! - `update_item` - JSON representation of the status update
|
|
//! - `uid` - "id" field of the update, used for deduplication
|
|
//!
|
|
//! Status updates are scheduled for sending by adding a record
|
|
//! to `smtp_status_updates_table` SQL table.
|
|
//! `smtp_status_updates` contains the following columns:
|
|
//! - `msg_id` - ID of the message in the `msgs` table
|
|
//! - `first_serial` - serial number of the first status update to send
|
|
//! - `last_serial` - serial number of the last status update to send
|
|
//! - `descr` - text to send along with the updates
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
|
|
|
|
use deltachat_derive::FromSql;
|
|
use lettre_email::PartBuilder;
|
|
use rusqlite::OptionalExtension;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use tokio::io::AsyncReadExt;
|
|
|
|
use crate::chat::{self, Chat};
|
|
use crate::constants::Chattype;
|
|
use crate::contact::ContactId;
|
|
use crate::context::Context;
|
|
use crate::download::DownloadState;
|
|
use crate::events::EventType;
|
|
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
|
use crate::mimefactory::wrapped_base64_encode;
|
|
use crate::mimeparser::SystemMessage;
|
|
use crate::param::Param;
|
|
use crate::param::Params;
|
|
use crate::tools::create_id;
|
|
use crate::tools::strip_rtlo_characters;
|
|
use crate::tools::{create_smeared_timestamp, get_abs_path};
|
|
|
|
/// The current API version.
|
|
/// If `min_api` in manifest.toml is set to a larger value,
|
|
/// the Webxdc's index.html is replaced by an error message.
|
|
/// In the future, that may be useful to avoid new Webxdc being loaded on old Delta Chats.
|
|
const WEBXDC_API_VERSION: u32 = 1;
|
|
|
|
/// Suffix used to recognize webxdc files.
|
|
pub const WEBXDC_SUFFIX: &str = "xdc";
|
|
const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
|
|
|
|
/// Raw information read from manifest.toml
|
|
#[derive(Debug, Deserialize, Default)]
|
|
#[non_exhaustive]
|
|
pub struct WebxdcManifest {
|
|
/// Webxdc name, used on icons or page titles.
|
|
pub name: Option<String>,
|
|
|
|
/// Minimum API version required to run this webxdc.
|
|
pub min_api: Option<u32>,
|
|
|
|
/// Optional URL of webxdc source code.
|
|
pub source_code_url: Option<String>,
|
|
|
|
/// If the webxdc requests network access.
|
|
pub request_internet_access: Option<bool>,
|
|
}
|
|
|
|
/// Parsed information from WebxdcManifest and fallbacks.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct WebxdcInfo {
|
|
/// The name of the app.
|
|
/// Defaults to filename if not set in the manifest.
|
|
pub name: String,
|
|
|
|
/// Filename of the app icon.
|
|
pub icon: String,
|
|
|
|
/// If the webxdc represents a document and allows to edit it,
|
|
/// this is the document name.
|
|
/// Otherwise an empty string.
|
|
pub document: String,
|
|
|
|
/// Short description of the webxdc state.
|
|
/// For example, "7 votes".
|
|
pub summary: String,
|
|
|
|
/// URL of webxdc source code or an empty string.
|
|
pub source_code_url: String,
|
|
|
|
/// If the webxdc is allowed to access the network.
|
|
/// It should request access, be encrypted
|
|
/// and sent to self for this.
|
|
pub internet_access: bool,
|
|
}
|
|
|
|
/// Status Update ID.
|
|
#[derive(
|
|
Debug,
|
|
Copy,
|
|
Clone,
|
|
Default,
|
|
PartialEq,
|
|
Eq,
|
|
Hash,
|
|
PartialOrd,
|
|
Ord,
|
|
Serialize,
|
|
Deserialize,
|
|
FromSql,
|
|
FromPrimitive,
|
|
)]
|
|
pub struct StatusUpdateSerial(u32);
|
|
|
|
impl StatusUpdateSerial {
|
|
/// Create a new [StatusUpdateSerial].
|
|
pub fn new(id: u32) -> StatusUpdateSerial {
|
|
StatusUpdateSerial(id)
|
|
}
|
|
|
|
/// Gets StatusUpdateSerial as untyped integer.
|
|
/// Avoid using this outside ffi.
|
|
pub fn to_u32(self) -> u32 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl rusqlite::types::ToSql for StatusUpdateSerial {
|
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
|
let val = rusqlite::types::Value::Integer(i64::from(self.0));
|
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
// Array of update items as sent on the wire.
|
|
#[derive(Debug, Deserialize)]
|
|
struct StatusUpdates {
|
|
updates: Vec<StatusUpdateItem>,
|
|
}
|
|
|
|
/// Update items as sent on the wire and as stored in the database.
|
|
#[derive(Debug, Serialize, Deserialize, Default)]
|
|
pub struct StatusUpdateItem {
|
|
/// The playload of the status update.
|
|
pub payload: Value,
|
|
|
|
/// Optional short info message that will be displayed in the chat.
|
|
/// For example "Alice added an item" or "Bob voted for option x".
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub info: Option<String>,
|
|
|
|
/// The new name of the editing document.
|
|
/// This is not needed if the webxdc doesn't edit documents.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub document: Option<String>,
|
|
|
|
/// Optional summary of the status update which will be shown next to the
|
|
/// app icon. This should be short and can be something like "8 votes"
|
|
/// for a voting app.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub summary: Option<String>,
|
|
|
|
/// Unique ID for deduplication.
|
|
/// This can be used if the message is sent over multiple transports.
|
|
///
|
|
/// If there is no ID, message is always considered to be unique.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub uid: Option<String>,
|
|
}
|
|
|
|
/// Update items as passed to the UIs.
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub(crate) struct StatusUpdateItemAndSerial {
|
|
#[serde(flatten)]
|
|
item: StatusUpdateItem,
|
|
|
|
serial: StatusUpdateSerial,
|
|
max_serial: StatusUpdateSerial,
|
|
}
|
|
|
|
/// Returns an entry index and a reference.
|
|
fn find_zip_entry<'a>(
|
|
file: &'a async_zip::ZipFile,
|
|
name: &str,
|
|
) -> Option<(usize, &'a async_zip::StoredZipEntry)> {
|
|
for (i, ent) in file.entries().iter().enumerate() {
|
|
if ent.entry().filename() == name {
|
|
return Some((i, ent));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
impl Context {
|
|
/// check if a file is an acceptable webxdc for sending or receiving.
|
|
pub(crate) async fn is_webxdc_file(&self, filename: &str, file: &[u8]) -> Result<bool> {
|
|
if !filename.ends_with(WEBXDC_SUFFIX) {
|
|
return Ok(false);
|
|
}
|
|
|
|
let archive = match async_zip::read::mem::ZipFileReader::new(file.to_vec()).await {
|
|
Ok(archive) => archive,
|
|
Err(_) => {
|
|
info!(self, "{} cannot be opened as zip-file", &filename);
|
|
return Ok(false);
|
|
}
|
|
};
|
|
|
|
if find_zip_entry(archive.file(), "index.html").is_none() {
|
|
info!(self, "{} misses index.html", &filename);
|
|
return Ok(false);
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Ensure that a file is an acceptable webxdc for sending.
|
|
pub(crate) async fn ensure_sendable_webxdc_file(&self, path: &Path) -> Result<()> {
|
|
let filename = path.to_str().unwrap_or_default();
|
|
if !filename.ends_with(WEBXDC_SUFFIX) {
|
|
bail!("{} is not a valid webxdc file", filename);
|
|
}
|
|
|
|
let valid = match async_zip::read::fs::ZipFileReader::new(path).await {
|
|
Ok(archive) => {
|
|
if find_zip_entry(archive.file(), "index.html").is_none() {
|
|
warn!(self, "{} misses index.html", filename);
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
Err(_) => {
|
|
warn!(self, "{} cannot be opened as zip-file", filename);
|
|
false
|
|
}
|
|
};
|
|
|
|
if !valid {
|
|
bail!("{} is not a valid webxdc file", filename);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if the last message of a chat is an info message belonging to the given instance and sender.
|
|
/// If so, the id of this message is returned.
|
|
async fn get_overwritable_info_msg_id(
|
|
&self,
|
|
instance: &Message,
|
|
from_id: ContactId,
|
|
) -> Result<Option<MsgId>> {
|
|
if let Some((last_msg_id, last_from_id, last_param, last_in_repl_to)) = self
|
|
.sql
|
|
.query_row_optional(
|
|
r#"SELECT id, from_id, param, mime_in_reply_to
|
|
FROM msgs
|
|
WHERE chat_id=?1 AND hidden=0
|
|
ORDER BY timestamp DESC, id DESC LIMIT 1"#,
|
|
(instance.chat_id,),
|
|
|row| {
|
|
let last_msg_id: MsgId = row.get(0)?;
|
|
let last_from_id: ContactId = row.get(1)?;
|
|
let last_param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
|
|
let last_in_repl_to: String = row.get(3)?;
|
|
Ok((last_msg_id, last_from_id, last_param, last_in_repl_to))
|
|
},
|
|
)
|
|
.await?
|
|
{
|
|
if last_from_id == from_id
|
|
&& last_param.get_cmd() == SystemMessage::WebxdcInfoMessage
|
|
&& last_in_repl_to == instance.rfc724_mid
|
|
{
|
|
return Ok(Some(last_msg_id));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
/// Takes an update-json as `{payload: PAYLOAD}`
|
|
/// writes it to the database and handles events, info-messages, document name and summary.
|
|
async fn create_status_update_record(
|
|
&self,
|
|
instance: &mut Message,
|
|
status_update_item: StatusUpdateItem,
|
|
timestamp: i64,
|
|
can_info_msg: bool,
|
|
from_id: ContactId,
|
|
) -> Result<Option<StatusUpdateSerial>> {
|
|
let Some(status_update_serial) = self
|
|
.write_status_update_inner(&instance.id, &status_update_item, timestamp)
|
|
.await?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
|
|
if can_info_msg {
|
|
if let Some(ref info) = status_update_item.info {
|
|
if let Some(info_msg_id) =
|
|
self.get_overwritable_info_msg_id(instance, from_id).await?
|
|
{
|
|
chat::update_msg_text_and_timestamp(
|
|
self,
|
|
instance.chat_id,
|
|
info_msg_id,
|
|
info.as_str(),
|
|
timestamp,
|
|
)
|
|
.await?;
|
|
} else {
|
|
chat::add_info_msg_with_cmd(
|
|
self,
|
|
instance.chat_id,
|
|
info.as_str(),
|
|
SystemMessage::WebxdcInfoMessage,
|
|
timestamp,
|
|
None,
|
|
Some(instance),
|
|
Some(from_id),
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut param_changed = false;
|
|
|
|
if let Some(ref document) = status_update_item.document {
|
|
if instance
|
|
.param
|
|
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
|
|
{
|
|
instance.param.set(Param::WebxdcDocument, document);
|
|
param_changed = true;
|
|
}
|
|
}
|
|
|
|
if let Some(ref summary) = status_update_item.summary {
|
|
if instance
|
|
.param
|
|
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
|
|
{
|
|
instance
|
|
.param
|
|
.set(Param::WebxdcSummary, strip_rtlo_characters(summary));
|
|
param_changed = true;
|
|
}
|
|
}
|
|
|
|
if param_changed {
|
|
instance.update_param(self).await?;
|
|
self.emit_msgs_changed(instance.chat_id, instance.id);
|
|
}
|
|
|
|
if instance.viewtype == Viewtype::Webxdc {
|
|
self.emit_event(EventType::WebxdcStatusUpdate {
|
|
msg_id: instance.id,
|
|
status_update_serial,
|
|
});
|
|
}
|
|
|
|
Ok(Some(status_update_serial))
|
|
}
|
|
|
|
/// Inserts a status update item into `msgs_status_updates` table.
|
|
///
|
|
/// Returns serial ID of the status update if a new item is inserted.
|
|
pub(crate) async fn write_status_update_inner(
|
|
&self,
|
|
instance_id: &MsgId,
|
|
status_update_item: &StatusUpdateItem,
|
|
timestamp: i64,
|
|
) -> Result<Option<StatusUpdateSerial>> {
|
|
let uid = status_update_item.uid.as_deref();
|
|
let status_update_item = serde_json::to_string(&status_update_item)?;
|
|
let trans_fn = |t: &mut rusqlite::Transaction| {
|
|
t.execute(
|
|
"UPDATE msgs SET timestamp_rcvd=? WHERE id=?",
|
|
(timestamp, instance_id),
|
|
)?;
|
|
let rowid = t
|
|
.query_row(
|
|
"INSERT INTO msgs_status_updates (msg_id, update_item, uid) VALUES(?, ?, ?)
|
|
ON CONFLICT (uid) DO NOTHING
|
|
RETURNING id",
|
|
(instance_id, status_update_item, uid),
|
|
|row| {
|
|
let id: u32 = row.get(0)?;
|
|
Ok(id)
|
|
},
|
|
)
|
|
.optional()?;
|
|
Ok(rowid)
|
|
};
|
|
let Some(rowid) = self.sql.transaction(trans_fn).await? else {
|
|
let uid = uid.unwrap_or("-");
|
|
info!(self, "Ignoring duplicate status update with uid={uid}");
|
|
return Ok(None);
|
|
};
|
|
let status_update_serial = StatusUpdateSerial(rowid);
|
|
Ok(Some(status_update_serial))
|
|
}
|
|
|
|
/// Returns the update_item with `status_update_serial` from the webxdc with message id `msg_id`.
|
|
pub async fn get_status_update(
|
|
&self,
|
|
msg_id: MsgId,
|
|
status_update_serial: StatusUpdateSerial,
|
|
) -> Result<String> {
|
|
self.sql
|
|
.query_get_value(
|
|
"SELECT update_item FROM msgs_status_updates WHERE id=? AND msg_id=? ",
|
|
(status_update_serial.0, msg_id),
|
|
)
|
|
.await?
|
|
.context("get_status_update: no update item found.")
|
|
}
|
|
|
|
/// Sends a status update for an webxdc instance.
|
|
///
|
|
/// If the instance is a draft,
|
|
/// the status update is sent once the instance is actually sent.
|
|
/// Otherwise, the update is sent as soon as possible.
|
|
pub async fn send_webxdc_status_update(
|
|
&self,
|
|
instance_msg_id: MsgId,
|
|
update_str: &str,
|
|
descr: &str,
|
|
) -> Result<()> {
|
|
let status_update_item: StatusUpdateItem = serde_json::from_str(update_str)
|
|
.with_context(|| format!("Failed to parse webxdc update item from {update_str:?}"))?;
|
|
self.send_webxdc_status_update_struct(instance_msg_id, status_update_item, descr)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Sends a status update for an webxdc instance.
|
|
/// Also see [Self::send_webxdc_status_update]
|
|
pub async fn send_webxdc_status_update_struct(
|
|
&self,
|
|
instance_msg_id: MsgId,
|
|
mut status_update: StatusUpdateItem,
|
|
descr: &str,
|
|
) -> Result<()> {
|
|
let mut instance = Message::load_from_db(self, instance_msg_id)
|
|
.await
|
|
.with_context(|| {
|
|
format!("Failed to load message {instance_msg_id} from the database")
|
|
})?;
|
|
let viewtype = instance.viewtype;
|
|
if viewtype != Viewtype::Webxdc {
|
|
bail!("send_webxdc_status_update: message {instance_msg_id} is not a webxdc message, but a {viewtype} message.");
|
|
}
|
|
|
|
let chat_id = instance.chat_id;
|
|
let chat = Chat::load_from_db(self, chat_id)
|
|
.await
|
|
.with_context(|| format!("Failed to load chat {chat_id} from the database"))?;
|
|
if let Some(reason) = chat.why_cant_send(self).await.with_context(|| {
|
|
format!("Failed to check if webxdc update can be sent to chat {chat_id}")
|
|
})? {
|
|
bail!("Cannot send to {chat_id}: {reason}.");
|
|
}
|
|
|
|
let send_now = !matches!(
|
|
instance.state,
|
|
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft
|
|
);
|
|
|
|
status_update.uid = Some(create_id());
|
|
let status_update_serial: StatusUpdateSerial = self
|
|
.create_status_update_record(
|
|
&mut instance,
|
|
status_update,
|
|
create_smeared_timestamp(self),
|
|
send_now,
|
|
ContactId::SELF,
|
|
)
|
|
.await
|
|
.context("Failed to create status update")?
|
|
.context("Duplicate status update UID was generated")?;
|
|
|
|
if send_now {
|
|
self.sql.insert(
|
|
"INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) VALUES(?, ?, ?, ?)
|
|
ON CONFLICT(msg_id)
|
|
DO UPDATE SET last_serial=excluded.last_serial, descr=excluded.descr",
|
|
(instance.id, status_update_serial, status_update_serial, descr),
|
|
).await.context("Failed to insert webxdc update into SMTP queue")?;
|
|
self.scheduler.interrupt_smtp().await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Pops one record of queued webxdc status updates.
|
|
/// This function exists to make the sqlite statement testable.
|
|
async fn pop_smtp_status_update(
|
|
&self,
|
|
) -> Result<Option<(MsgId, StatusUpdateSerial, StatusUpdateSerial, String)>> {
|
|
let _lock = self.sql.write_lock().await;
|
|
let res = self
|
|
.sql
|
|
.query_row_optional(
|
|
"DELETE FROM smtp_status_updates
|
|
WHERE msg_id IN (SELECT msg_id FROM smtp_status_updates LIMIT 1)
|
|
RETURNING msg_id, first_serial, last_serial, descr",
|
|
(),
|
|
|row| {
|
|
let instance_id: MsgId = row.get(0)?;
|
|
let first_serial: StatusUpdateSerial = row.get(1)?;
|
|
let last_serial: StatusUpdateSerial = row.get(2)?;
|
|
let descr: String = row.get(3)?;
|
|
Ok((instance_id, first_serial, last_serial, descr))
|
|
},
|
|
)
|
|
.await?;
|
|
Ok(res)
|
|
}
|
|
|
|
/// Attempts to send queued webxdc status updates.
|
|
pub(crate) async fn flush_status_updates(&self) -> Result<()> {
|
|
loop {
|
|
let (instance_id, first_serial, last_serial, descr) =
|
|
match self.pop_smtp_status_update().await? {
|
|
Some(res) => res,
|
|
None => return Ok(()),
|
|
};
|
|
|
|
if let Some(json) = self
|
|
.render_webxdc_status_update_object(instance_id, Some((first_serial, last_serial)))
|
|
.await?
|
|
{
|
|
let instance = Message::load_from_db(self, instance_id).await?;
|
|
let mut status_update = Message {
|
|
chat_id: instance.chat_id,
|
|
viewtype: Viewtype::Text,
|
|
text: descr.to_string(),
|
|
hidden: true,
|
|
..Default::default()
|
|
};
|
|
status_update
|
|
.param
|
|
.set_cmd(SystemMessage::WebxdcStatusUpdate);
|
|
status_update.param.set(Param::Arg, json);
|
|
status_update.set_quote(self, Some(&instance)).await?;
|
|
status_update.param.remove(Param::GuaranteeE2ee); // may be set by set_quote(), if #2985 is done, this line can be removed
|
|
chat::send_msg(self, instance.chat_id, &mut status_update).await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn build_status_update_part(&self, json: &str) -> PartBuilder {
|
|
let encoded_body = wrapped_base64_encode(json.as_bytes());
|
|
|
|
PartBuilder::new()
|
|
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
|
.header((
|
|
"Content-Disposition",
|
|
"attachment; filename=\"status-update.json\"",
|
|
))
|
|
.header(("Content-Transfer-Encoding", "base64"))
|
|
.body(encoded_body)
|
|
}
|
|
|
|
/// Receives status updates from receive_imf to the database
|
|
/// and sends out an event.
|
|
///
|
|
/// `from_id` is the sender
|
|
///
|
|
/// `msg_id` may be an instance (in case there are initial status updates)
|
|
/// or a reply to an instance (for all other updates).
|
|
///
|
|
/// `json` is an array containing one or more update items as created by send_webxdc_status_update(),
|
|
/// the array is parsed using serde, the single payloads are used as is.
|
|
pub(crate) async fn receive_status_update(
|
|
&self,
|
|
from_id: ContactId,
|
|
msg_id: MsgId,
|
|
json: &str,
|
|
) -> Result<()> {
|
|
let msg = Message::load_from_db(self, msg_id).await?;
|
|
let (timestamp, mut instance, can_info_msg) = if msg.viewtype == Viewtype::Webxdc {
|
|
(msg.timestamp_sort, msg, false)
|
|
} else if let Some(parent) = msg.parent(self).await? {
|
|
if parent.viewtype == Viewtype::Webxdc {
|
|
(msg.timestamp_sort, parent, true)
|
|
} else if parent.download_state() != DownloadState::Done {
|
|
(msg.timestamp_sort, parent, false)
|
|
} else {
|
|
bail!("receive_status_update: message is not the child of a webxdc message.")
|
|
}
|
|
} else {
|
|
bail!("receive_status_update: status message has no parent.")
|
|
};
|
|
let chat_id = instance.chat_id;
|
|
|
|
if from_id != ContactId::SELF && !chat::is_contact_in_chat(self, chat_id, from_id).await? {
|
|
let chat_type: Chattype = self
|
|
.sql
|
|
.query_get_value("SELECT type FROM chats WHERE id=?", (chat_id,))
|
|
.await?
|
|
.with_context(|| format!("Chat type for chat {chat_id} not found"))?;
|
|
if chat_type != Chattype::Mailinglist {
|
|
bail!("receive_status_update: status sender {from_id} is not a member of chat {chat_id}")
|
|
}
|
|
}
|
|
|
|
let updates: StatusUpdates = serde_json::from_str(json)?;
|
|
for update_item in updates.updates {
|
|
self.create_status_update_record(
|
|
&mut instance,
|
|
update_item,
|
|
timestamp,
|
|
can_info_msg,
|
|
from_id,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns status updates as an JSON-array, ready to be consumed by a webxdc.
|
|
///
|
|
/// Example: `[{"serial":1, "max_serial":3, "payload":"any update data"},
|
|
/// {"serial":3, "max_serial":3, "payload":"another update data"}]`
|
|
/// Updates with serials larger than `last_known_serial` are returned.
|
|
/// If no last serial is known, set `last_known_serial` to 0.
|
|
/// If no updates are available, an empty JSON-array is returned.
|
|
pub async fn get_webxdc_status_updates(
|
|
&self,
|
|
instance_msg_id: MsgId,
|
|
last_known_serial: StatusUpdateSerial,
|
|
) -> Result<String> {
|
|
let json = self
|
|
.sql
|
|
.query_map(
|
|
"SELECT update_item, id FROM msgs_status_updates WHERE msg_id=? AND id>? ORDER BY id",
|
|
(instance_msg_id, last_known_serial),
|
|
|row| {
|
|
let update_item_str = row.get::<_, String>(0)?;
|
|
let serial = row.get::<_, StatusUpdateSerial>(1)?;
|
|
Ok((update_item_str, serial))
|
|
},
|
|
|rows| {
|
|
let mut rows_copy : Vec<(String, StatusUpdateSerial)> = Vec::new(); // `rows_copy` needed as `rows` cannot be iterated twice.
|
|
let mut max_serial = StatusUpdateSerial(0);
|
|
for row in rows {
|
|
let row = row?;
|
|
if row.1 > max_serial {
|
|
max_serial = row.1;
|
|
}
|
|
rows_copy.push(row);
|
|
}
|
|
|
|
let mut json = String::default();
|
|
for row in rows_copy {
|
|
let (update_item_str, serial) = row;
|
|
let update_item = StatusUpdateItemAndSerial
|
|
{
|
|
item: StatusUpdateItem {
|
|
uid: None, // Erase UIDs, apps, bots and tests don't need to know them.
|
|
..serde_json::from_str(&update_item_str)?
|
|
},
|
|
serial,
|
|
max_serial,
|
|
};
|
|
|
|
if !json.is_empty() {
|
|
json.push_str(",\n");
|
|
}
|
|
json.push_str(&serde_json::to_string(&update_item)?);
|
|
}
|
|
Ok(json)
|
|
},
|
|
)
|
|
.await?;
|
|
Ok(format!("[{json}]"))
|
|
}
|
|
|
|
/// Renders JSON-object for status updates as used on the wire.
|
|
///
|
|
/// Example: `{"updates": [{"payload":"any update data"},
|
|
/// {"payload":"another update data"}]}`
|
|
///
|
|
/// `range` is an optional range of status update serials to send.
|
|
/// If it is `None`, all updates are sent.
|
|
/// This is used when a message is resent using [`crate::chat::resend_msgs`].
|
|
pub(crate) async fn render_webxdc_status_update_object(
|
|
&self,
|
|
instance_msg_id: MsgId,
|
|
range: Option<(StatusUpdateSerial, StatusUpdateSerial)>,
|
|
) -> Result<Option<String>> {
|
|
let json = self
|
|
.sql
|
|
.query_map(
|
|
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND id>=? AND id<=? ORDER BY id",
|
|
(
|
|
instance_msg_id,
|
|
range.map(|r|r.0).unwrap_or(StatusUpdateSerial(0)),
|
|
range.map(|r|r.1).unwrap_or(StatusUpdateSerial(u32::MAX)),
|
|
),
|
|
|row| row.get::<_, String>(0),
|
|
|rows| {
|
|
let mut json = String::default();
|
|
for row in rows {
|
|
let update_item = row?;
|
|
if !json.is_empty() {
|
|
json.push_str(",\n");
|
|
}
|
|
json.push_str(&update_item);
|
|
}
|
|
Ok(json)
|
|
},
|
|
)
|
|
.await?;
|
|
if json.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(format!(r#"{{"updates":[{json}]}}"#)))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
|
|
let s = std::str::from_utf8(bytes)?;
|
|
let manifest: WebxdcManifest = toml::from_str(s)?;
|
|
Ok(manifest)
|
|
}
|
|
|
|
async fn get_blob(archive: &async_zip::read::fs::ZipFileReader, name: &str) -> Result<Vec<u8>> {
|
|
let (i, _) = find_zip_entry(archive.file(), name)
|
|
.ok_or_else(|| anyhow!("no entry found for {}", name))?;
|
|
let mut reader = archive.entry(i).await?;
|
|
let mut buf = Vec::new();
|
|
reader.read_to_end(&mut buf).await?;
|
|
Ok(buf)
|
|
}
|
|
|
|
impl Message {
|
|
/// Get handle to a webxdc ZIP-archive.
|
|
/// To check for file existence use archive.by_name(), to read a file, use get_blob(archive).
|
|
async fn get_webxdc_archive(
|
|
&self,
|
|
context: &Context,
|
|
) -> Result<async_zip::read::fs::ZipFileReader> {
|
|
let path = self
|
|
.get_file(context)
|
|
.ok_or_else(|| format_err!("No webxdc instance file."))?;
|
|
let path_abs = get_abs_path(context, &path);
|
|
let archive = async_zip::read::fs::ZipFileReader::new(path_abs).await?;
|
|
Ok(archive)
|
|
}
|
|
|
|
/// Return file from inside an archive.
|
|
/// Currently, this works only if the message is an webxdc instance.
|
|
///
|
|
/// `name` is the filename within the archive, e.g. `index.html`.
|
|
pub async fn get_webxdc_blob(&self, context: &Context, name: &str) -> Result<Vec<u8>> {
|
|
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
|
|
|
|
if name == WEBXDC_DEFAULT_ICON {
|
|
return Ok(include_bytes!("../assets/icon-webxdc.png").to_vec());
|
|
}
|
|
|
|
// ignore first slash.
|
|
// this way, files can be accessed absolutely (`/index.html`) as well as relatively (`index.html`)
|
|
let name = if name.starts_with('/') {
|
|
name.split_at(1).1
|
|
} else {
|
|
name
|
|
};
|
|
|
|
let archive = self.get_webxdc_archive(context).await?;
|
|
|
|
if name == "index.html" {
|
|
if let Ok(bytes) = get_blob(&archive, "manifest.toml").await {
|
|
if let Ok(manifest) = parse_webxdc_manifest(&bytes) {
|
|
if let Some(min_api) = manifest.min_api {
|
|
if min_api > WEBXDC_API_VERSION {
|
|
return Ok(Vec::from(
|
|
"<!DOCTYPE html>This Webxdc requires a newer Delta Chat version.",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get_blob(&archive, name).await
|
|
}
|
|
|
|
/// Return info from manifest.toml or from fallbacks.
|
|
pub async fn get_webxdc_info(&self, context: &Context) -> Result<WebxdcInfo> {
|
|
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
|
|
let archive = self.get_webxdc_archive(context).await?;
|
|
|
|
let mut manifest = get_blob(&archive, "manifest.toml")
|
|
.await
|
|
.map(|bytes| parse_webxdc_manifest(&bytes).unwrap_or_default())
|
|
.unwrap_or_default();
|
|
|
|
if let Some(ref name) = manifest.name {
|
|
let name = name.trim();
|
|
if name.is_empty() {
|
|
warn!(context, "empty name given in manifest");
|
|
manifest.name = None;
|
|
}
|
|
}
|
|
|
|
let internet_access = manifest.request_internet_access.unwrap_or_default()
|
|
&& self.chat_id.is_self_talk(context).await.unwrap_or_default()
|
|
&& self.get_showpadlock();
|
|
|
|
Ok(WebxdcInfo {
|
|
name: if let Some(name) = manifest.name {
|
|
name
|
|
} else {
|
|
self.get_filename().unwrap_or_default()
|
|
},
|
|
icon: if find_zip_entry(archive.file(), "icon.png").is_some() {
|
|
"icon.png".to_string()
|
|
} else if find_zip_entry(archive.file(), "icon.jpg").is_some() {
|
|
"icon.jpg".to_string()
|
|
} else {
|
|
WEBXDC_DEFAULT_ICON.to_string()
|
|
},
|
|
document: self
|
|
.param
|
|
.get(Param::WebxdcDocument)
|
|
.unwrap_or_default()
|
|
.to_string(),
|
|
summary: if internet_access {
|
|
"Dev Mode: Do not enter sensitive data!".to_string()
|
|
} else {
|
|
self.param
|
|
.get(Param::WebxdcSummary)
|
|
.unwrap_or_default()
|
|
.to_string()
|
|
},
|
|
source_code_url: if let Some(url) = manifest.source_code_url {
|
|
url
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
internet_access,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::time::Duration;
|
|
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
use crate::chat::{
|
|
add_contact_to_chat, create_broadcast_list, create_group_chat, forward_msgs,
|
|
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, ChatId, ProtectionStatus,
|
|
};
|
|
use crate::chatlist::Chatlist;
|
|
use crate::config::Config;
|
|
use crate::contact::Contact;
|
|
use crate::ephemeral;
|
|
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
|
use crate::test_utils::TestContext;
|
|
use crate::tools::{self, SystemTime};
|
|
use crate::{message, sql};
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_is_webxdc_file() -> Result<()> {
|
|
let t = TestContext::new().await;
|
|
assert!(
|
|
!t.is_webxdc_file(
|
|
"bad-ext-no-zip.txt",
|
|
include_bytes!("../test-data/message/issue_523.txt")
|
|
)
|
|
.await?
|
|
);
|
|
assert!(
|
|
!t.is_webxdc_file(
|
|
"bad-ext-good-zip.txt",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc")
|
|
)
|
|
.await?
|
|
);
|
|
assert!(
|
|
!t.is_webxdc_file(
|
|
"good-ext-no-zip.xdc",
|
|
include_bytes!("../test-data/message/issue_523.txt")
|
|
)
|
|
.await?
|
|
);
|
|
assert!(
|
|
!t.is_webxdc_file(
|
|
"good-ext-no-index-html.xdc",
|
|
include_bytes!("../test-data/webxdc/no-index-html.xdc")
|
|
)
|
|
.await?
|
|
);
|
|
assert!(
|
|
t.is_webxdc_file(
|
|
"good-ext-good-zip.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc")
|
|
)
|
|
.await?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result<Message> {
|
|
let mut instance = Message::new(Viewtype::File);
|
|
instance.set_file_from_bytes(t, name, bytes, None).await?;
|
|
Ok(instance)
|
|
}
|
|
|
|
async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result<Message> {
|
|
let mut instance = create_webxdc_instance(
|
|
t,
|
|
"minimal.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
|
)
|
|
.await?;
|
|
let instance_msg_id = send_msg(t, chat_id, &mut instance).await?;
|
|
assert_eq!(instance.viewtype, Viewtype::Webxdc);
|
|
Message::load_from_db(t, instance_msg_id).await
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_webxdc_instance() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
|
|
// send as .xdc file
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
assert_eq!(instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(instance.get_filename(), Some("minimal.xdc".to_string()));
|
|
assert_eq!(instance.chat_id, chat_id);
|
|
|
|
// sending using bad extension is not working, even when setting Viewtype to webxdc
|
|
let mut instance = Message::new(Viewtype::Webxdc);
|
|
instance
|
|
.set_file_from_bytes(&t, "index.html", b"<html>ola!</html>", None)
|
|
.await?;
|
|
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_invalid_webxdc() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
|
|
// sending invalid .xdc as file is possible, but must not result in Viewtype::Webxdc
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"invalid-no-zip-but-7z.xdc",
|
|
include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
|
|
)
|
|
.await?;
|
|
let instance_id = send_msg(&t, chat_id, &mut instance).await?;
|
|
assert_eq!(instance.viewtype, Viewtype::File);
|
|
let test = Message::load_from_db(&t, instance_id).await?;
|
|
assert_eq!(test.viewtype, Viewtype::File);
|
|
|
|
// sending invalid .xdc as Viewtype::Webxdc should fail already on sending
|
|
let mut instance = Message::new(Viewtype::Webxdc);
|
|
instance
|
|
.set_file_from_bytes(
|
|
&t,
|
|
"invalid2.xdc",
|
|
include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
|
|
None,
|
|
)
|
|
.await?;
|
|
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_special_webxdc_format() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
|
|
// chess.xdc is failing for some zip-versions, see #3476, if we know more details about why, we can have a nicer name for the test :)
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"chess.xdc",
|
|
include_bytes!("../test-data/webxdc/chess.xdc"),
|
|
)
|
|
.await?;
|
|
let instance_id = send_msg(&t, chat_id, &mut instance).await?;
|
|
let instance = Message::load_from_db(&t, instance_id).await?;
|
|
assert_eq!(instance.viewtype, Viewtype::Webxdc);
|
|
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "Chess Board");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_forward_webxdc_instance() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
t.send_webxdc_status_update(
|
|
instance.id,
|
|
r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#,
|
|
"descr",
|
|
)
|
|
.await?;
|
|
assert!(!instance.is_forwarded());
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"#
|
|
);
|
|
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
|
|
let info = Message::load_from_db(&t, instance.id)
|
|
.await?
|
|
.get_webxdc_info(&t)
|
|
.await?;
|
|
assert_eq!(info.summary, "bar".to_string());
|
|
assert_eq!(info.document, "doc".to_string());
|
|
|
|
// forwarding an instance creates a fresh instance; updates etc. are not forwarded
|
|
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
|
|
let instance2 = t.get_last_msg_in(chat_id).await;
|
|
assert!(instance2.is_forwarded());
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance2.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
"[]"
|
|
);
|
|
assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); // two instances, only one info
|
|
let info = Message::load_from_db(&t, instance2.id)
|
|
.await?
|
|
.get_webxdc_info(&t)
|
|
.await?;
|
|
assert_eq!(info.summary, "".to_string());
|
|
assert_eq!(info.document, "".to_string());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_resend_webxdc_instance_and_info() -> Result<()> {
|
|
// Alice uses webxdc in a group
|
|
let alice = TestContext::new_alice().await;
|
|
alice.set_config_bool(Config::BccSelf, false).await?;
|
|
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_grp).await?;
|
|
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 1);
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"payload":7,"info": "i","summary":"s"}"#,
|
|
"d",
|
|
)
|
|
.await?;
|
|
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 2);
|
|
assert!(alice.get_last_msg_in(alice_grp).await.is_info());
|
|
|
|
// Alice adds Bob and resend already used webxdc
|
|
add_contact_to_chat(
|
|
&alice,
|
|
alice_grp,
|
|
Contact::create(&alice, "", "bob@example.net").await?,
|
|
)
|
|
.await?;
|
|
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3);
|
|
resend_msgs(&alice, &[alice_instance.id]).await?;
|
|
let sent1 = alice.pop_sent_msg().await;
|
|
|
|
// Bob received webxdc, legacy info-messages updates are received but not added to the chat
|
|
let bob = TestContext::new_bob().await;
|
|
let bob_instance = bob.recv_msg(&sent1).await;
|
|
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
|
assert!(!bob_instance.is_info());
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"#
|
|
);
|
|
let bob_grp = bob_instance.chat_id;
|
|
assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id);
|
|
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_receive_webxdc_instance() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
receive_imf(
|
|
&t,
|
|
include_bytes!("../test-data/message/webxdc_good_extension.eml"),
|
|
false,
|
|
)
|
|
.await?;
|
|
let instance = t.get_last_msg().await;
|
|
assert_eq!(instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(instance.get_filename().unwrap(), "minimal.xdc");
|
|
|
|
receive_imf(
|
|
&t,
|
|
include_bytes!("../test-data/message/webxdc_bad_extension.eml"),
|
|
false,
|
|
)
|
|
.await?;
|
|
let instance = t.get_last_msg().await;
|
|
assert_eq!(instance.viewtype, Viewtype::File); // we require the correct extension, only a mime type is not sufficient
|
|
assert_eq!(instance.get_filename().unwrap(), "index.html");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_contact_request() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Alice sends an webxdc instance to Bob
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let _alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
|
|
|
// Bob can start the webxdc from a contact request (get index.html)
|
|
// but cannot send updates to contact requests
|
|
let bob_instance = bob.get_last_msg().await;
|
|
let bob_chat = Chat::load_from_db(&bob, bob_instance.chat_id).await?;
|
|
assert!(bob_chat.is_contact_request());
|
|
assert!(bob_instance
|
|
.get_webxdc_blob(&bob, "index.html")
|
|
.await
|
|
.is_ok());
|
|
assert!(bob
|
|
.send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#, "descr")
|
|
.await
|
|
.is_err());
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
"[]"
|
|
);
|
|
|
|
// Once the contact request is accepted, Bob can send updates
|
|
bob_chat.id.accept(&bob).await?;
|
|
assert!(bob
|
|
.send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#, "descr")
|
|
.await
|
|
.is_ok());
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":42,"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> {
|
|
// Alice sends a larger instance and an update
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
let chat = alice.create_chat(&bob).await;
|
|
bob.set_config(Config::DownloadLimit, Some("40000")).await?;
|
|
let mut alice_instance = create_webxdc_instance(
|
|
&alice,
|
|
"chess.xdc",
|
|
include_bytes!("../test-data/webxdc/chess.xdc"),
|
|
)
|
|
.await?;
|
|
let sent1 = alice.send_msg(chat.id, &mut alice_instance).await;
|
|
let alice_instance = sent1.load_from_db().await;
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"payload": 7, "summary":"sum", "document":"doc"}"#,
|
|
"bla",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent2 = alice.pop_sent_msg().await;
|
|
|
|
// Bob does not download instance but already receives update
|
|
receive_imf_from_inbox(
|
|
&bob,
|
|
&alice_instance.rfc724_mid,
|
|
sent1.payload().as_bytes(),
|
|
false,
|
|
Some(70790),
|
|
false,
|
|
)
|
|
.await?;
|
|
let bob_instance = bob.get_last_msg().await;
|
|
bob_instance.chat_id.accept(&bob).await?;
|
|
bob.recv_msg(&sent2).await;
|
|
assert_eq!(bob_instance.download_state, DownloadState::Available);
|
|
|
|
// Bob downloads instance, updates should be assigned correctly
|
|
let received_msg = receive_imf_from_inbox(
|
|
&bob,
|
|
&alice_instance.rfc724_mid,
|
|
sent1.payload().as_bytes(),
|
|
false,
|
|
None,
|
|
false,
|
|
)
|
|
.await?
|
|
.unwrap();
|
|
assert_eq!(*received_msg.msg_ids.first().unwrap(), bob_instance.id);
|
|
let bob_instance = bob.get_last_msg().await;
|
|
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(bob_instance.download_state, DownloadState::Done);
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":7,"document":"doc","summary":"sum","serial":1,"max_serial":1}]"#
|
|
);
|
|
let info = bob_instance.get_webxdc_info(&bob).await?;
|
|
assert_eq!(info.document, "doc");
|
|
assert_eq!(info.summary, "sum");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_delete_webxdc_instance() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
t.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#"{"updates":[{"payload":1}]}"#,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?,
|
|
1
|
|
);
|
|
|
|
message::delete_msgs(&t, &[instance.id]).await?;
|
|
sql::housekeeping(&t).await?;
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?,
|
|
0
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_delete_chat_with_webxdc() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
t.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#"{"updates":[{"payload":1}, {"payload":2}]}"#,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?,
|
|
2
|
|
);
|
|
|
|
chat_id.delete(&t).await?;
|
|
sql::housekeeping(&t).await?;
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?,
|
|
0
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_delete_webxdc_draft() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"minimal.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let instance = chat_id.get_draft(&t).await?.unwrap();
|
|
t.send_webxdc_status_update(instance.id, r#"{"payload": 42}"#, "descr")
|
|
.await?;
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":42,"serial":1,"max_serial":1}]"#.to_string()
|
|
);
|
|
|
|
// set_draft(None) deletes the message without the need to simulate network
|
|
chat_id.set_draft(&t, None).await?;
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
"[]".to_string()
|
|
);
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?,
|
|
0
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_status_update_record() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let mut instance = send_webxdc_instance(&t, chat_id).await?;
|
|
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
"[]"
|
|
);
|
|
|
|
let update_id1 = t
|
|
.create_status_update_record(
|
|
&mut instance,
|
|
StatusUpdateItem {
|
|
payload: json!({"foo": "bar"}),
|
|
info: None,
|
|
document: None,
|
|
summary: None,
|
|
uid: Some("iecie2Ze".to_string()),
|
|
},
|
|
1640178619,
|
|
true,
|
|
ContactId::SELF,
|
|
)
|
|
.await?
|
|
.unwrap();
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
// Update with duplicate update ID is received.
|
|
// Whatever the payload is, update should be ignored just because ID is duplicate.
|
|
let update_id1_duplicate = t
|
|
.create_status_update_record(
|
|
&mut instance,
|
|
StatusUpdateItem {
|
|
payload: json!({"nothing": "this should be ignored"}),
|
|
info: None,
|
|
document: None,
|
|
summary: None,
|
|
uid: Some("iecie2Ze".to_string()),
|
|
},
|
|
1640178619,
|
|
true,
|
|
ContactId::SELF,
|
|
)
|
|
.await?;
|
|
assert_eq!(update_id1_duplicate, None);
|
|
|
|
assert!(t
|
|
.send_webxdc_status_update(instance.id, "\n\n\n", "")
|
|
.await
|
|
.is_err());
|
|
|
|
assert!(t
|
|
.send_webxdc_status_update(instance.id, "bad json", "")
|
|
.await
|
|
.is_err());
|
|
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
let update_id2 = t
|
|
.create_status_update_record(
|
|
&mut instance,
|
|
StatusUpdateItem {
|
|
payload: json!({"foo2": "bar2"}),
|
|
info: None,
|
|
document: None,
|
|
summary: None,
|
|
uid: None,
|
|
},
|
|
1640178619,
|
|
true,
|
|
ContactId::SELF,
|
|
)
|
|
.await?
|
|
.unwrap();
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, update_id1).await?,
|
|
r#"[{"payload":{"foo2":"bar2"},"serial":3,"max_serial":3}]"#
|
|
);
|
|
t.create_status_update_record(
|
|
&mut instance,
|
|
StatusUpdateItem {
|
|
payload: Value::Bool(true),
|
|
info: None,
|
|
document: None,
|
|
summary: None,
|
|
uid: None,
|
|
},
|
|
1640178619,
|
|
true,
|
|
ContactId::SELF,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4},
|
|
{"payload":{"foo2":"bar2"},"serial":3,"max_serial":4},
|
|
{"payload":true,"serial":4,"max_serial":4}]"#
|
|
);
|
|
|
|
t.send_webxdc_status_update(
|
|
instance.id,
|
|
r#"{"payload" : 1, "sender": "that is not used"}"#,
|
|
"",
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, update_id2).await?,
|
|
r#"[{"payload":true,"serial":4,"max_serial":5},
|
|
{"payload":1,"serial":5,"max_serial":5}]"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_receive_status_update() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
|
|
assert!(t
|
|
.receive_status_update(ContactId::SELF, instance.id, r#"foo: bar"#)
|
|
.await
|
|
.is_err()); // no json
|
|
assert!(t
|
|
.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#"{"updada":[{"payload":{"foo":"bar"}}]}"#
|
|
)
|
|
.await
|
|
.is_err()); // "updates" object missing
|
|
assert!(t
|
|
.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#"{"updates":[{"foo":"bar"}]}"#
|
|
)
|
|
.await
|
|
.is_err()); // "payload" field missing
|
|
assert!(t
|
|
.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#"{"updates":{"payload":{"foo":"bar"}}}"#
|
|
)
|
|
.await
|
|
.is_err()); // not an array
|
|
|
|
t.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#"{"updates":[{"payload":{"foo":"bar"}, "someTrash": "definitely TrAsH"}]}"#,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
t.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#" {"updates": [ {"payload" :42} , {"payload": 23} ] } "#,
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3},
|
|
{"payload":42,"serial":2,"max_serial":3},
|
|
{"payload":23,"serial":3,"max_serial":3}]"#
|
|
);
|
|
|
|
t.receive_status_update(
|
|
ContactId::SELF,
|
|
instance.id,
|
|
r#" {"updates": [ {"payload" :"ok", "future_item": "test"} ], "from": "future" } "#,
|
|
)
|
|
.await?; // ignore members that may be added in the future
|
|
assert_eq!(
|
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4},
|
|
{"payload":42,"serial":2,"max_serial":4},
|
|
{"payload":23,"serial":3,"max_serial":4},
|
|
{"payload":"ok","serial":4,"max_serial":4}]"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn expect_status_update_event(t: &TestContext, instance_id: MsgId) -> Result<()> {
|
|
let event = t
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. }))
|
|
.await;
|
|
match event {
|
|
EventType::WebxdcStatusUpdate {
|
|
msg_id,
|
|
status_update_serial: _,
|
|
} => {
|
|
assert_eq!(msg_id, instance_id);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_webxdc_status_update() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
alice.set_config_bool(Config::BccSelf, true).await?;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Alice sends an webxdc instance and a status update
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
let sent1 = &alice.pop_sent_msg().await;
|
|
assert_eq!(alice_instance.viewtype, Viewtype::Webxdc);
|
|
assert!(!sent1.payload().contains("report-type=status-update"));
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"payload" : {"foo":"bar"}}"#,
|
|
"descr text",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
expect_status_update_event(&alice, alice_instance.id).await?;
|
|
let sent2 = &alice.pop_sent_msg().await;
|
|
let alice_update = sent2.load_from_db().await;
|
|
assert!(alice_update.hidden);
|
|
assert_eq!(alice_update.viewtype, Viewtype::Text);
|
|
assert_eq!(alice_update.get_filename(), None);
|
|
assert_eq!(alice_update.text, "descr text".to_string());
|
|
assert_eq!(alice_update.chat_id, alice_instance.chat_id);
|
|
assert_eq!(
|
|
alice_update.parent(&alice).await?.unwrap().id,
|
|
alice_instance.id
|
|
);
|
|
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
|
assert!(sent2.payload().contains("report-type=status-update"));
|
|
assert!(sent2.payload().contains("descr text"));
|
|
assert_eq!(
|
|
alice
|
|
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"payload":{"snipp":"snapp"}}"#,
|
|
"bla text",
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
alice
|
|
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
|
|
{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"#
|
|
);
|
|
|
|
// Bob receives all messages
|
|
let bob_instance = bob.recv_msg(sent1).await;
|
|
let bob_chat_id = bob_instance.chat_id;
|
|
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
|
|
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
|
|
|
bob.recv_msg(sent2).await;
|
|
expect_status_update_event(&bob, bob_instance.id).await?;
|
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
|
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
// Alice has a second device and also receives messages there
|
|
let alice2 = TestContext::new_alice().await;
|
|
alice2.recv_msg(sent1).await;
|
|
alice2.recv_msg(sent2).await;
|
|
let alice2_instance = alice2.get_last_msg().await;
|
|
let alice2_chat_id = alice2_instance.chat_id;
|
|
assert_eq!(alice2_instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 1);
|
|
|
|
// To support the second device, Alice has enabled bcc_self and will receive their own messages;
|
|
// these messages, however, should be ignored
|
|
alice.recv_msg_opt(sent1).await;
|
|
alice.recv_msg_opt(sent2).await;
|
|
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
|
assert_eq!(
|
|
alice
|
|
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
|
|
{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_render_webxdc_status_update_object() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"minimal.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
assert!(t
|
|
.render_webxdc_status_update_object(instance.id, None)
|
|
.await?
|
|
.is_none());
|
|
|
|
t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#, "bla")
|
|
.await?;
|
|
assert!(t
|
|
.render_webxdc_status_update_object(instance.id, None)
|
|
.await?
|
|
.is_some());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_render_webxdc_status_update_object_range() -> Result<()> {
|
|
use regex::Regex;
|
|
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#, "d")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance.id, r#"{"payload": 2}"#, "d")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance.id, r#"{"payload": 3}"#, "d")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance.id, r#"{"payload": 4}"#, "d")
|
|
.await?;
|
|
let json = t
|
|
.render_webxdc_status_update_object(
|
|
instance.id,
|
|
Some((StatusUpdateSerial(2), StatusUpdateSerial(3))),
|
|
)
|
|
.await?
|
|
.unwrap();
|
|
let json = Regex::new(r#""uid":"[^"]*""#)
|
|
.unwrap()
|
|
.replace_all(&json, "XXX");
|
|
assert_eq!(
|
|
json,
|
|
"{\"updates\":[{\"payload\":2,XXX},\n{\"payload\":3,XXX}]}"
|
|
);
|
|
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM smtp_status_updates", ())
|
|
.await?,
|
|
1
|
|
);
|
|
t.flush_status_updates().await?;
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM smtp_status_updates", ())
|
|
.await?,
|
|
0
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_pop_status_update() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
|
|
let instance1 = send_webxdc_instance(&t, chat_id).await?;
|
|
let instance2 = send_webxdc_instance(&t, chat_id).await?;
|
|
let instance3 = send_webxdc_instance(&t, chat_id).await?;
|
|
assert!(t.pop_smtp_status_update().await?.is_none());
|
|
|
|
t.send_webxdc_status_update(instance1.id, r#"{"payload": "1a"}"#, "descr1a")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance2.id, r#"{"payload": "2a"}"#, "descr2a")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance2.id, r#"{"payload": "2b"}"#, "descr2b")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance3.id, r#"{"payload": "3a"}"#, "descr3a")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance3.id, r#"{"payload": "3b"}"#, "descr3b")
|
|
.await?;
|
|
t.send_webxdc_status_update(instance3.id, r#"{"payload": "3c"}"#, "descr3c")
|
|
.await?;
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM smtp_status_updates", ())
|
|
.await?,
|
|
3
|
|
);
|
|
|
|
// order of pop_status_update() is not defined, therefore the more complicated test
|
|
let mut instances_checked = 0;
|
|
for i in 0..3 {
|
|
let (instance, min_ser, max_ser, descr) = t.pop_smtp_status_update().await?.unwrap();
|
|
if instance == instance1.id {
|
|
assert_eq!(min_ser, max_ser);
|
|
assert_eq!(descr, "descr1a");
|
|
instances_checked += 1;
|
|
} else if instance == instance2.id {
|
|
assert_eq!(min_ser.to_u32(), max_ser.to_u32() - 1);
|
|
assert_eq!(descr, "descr2b");
|
|
instances_checked += 1;
|
|
} else if instance == instance3.id {
|
|
assert_eq!(min_ser.to_u32(), max_ser.to_u32() - 2);
|
|
assert_eq!(descr, "descr3c");
|
|
instances_checked += 1;
|
|
} else {
|
|
bail!("unexpected instance");
|
|
}
|
|
assert_eq!(
|
|
t.sql
|
|
.count("SELECT COUNT(*) FROM smtp_status_updates", ())
|
|
.await?,
|
|
2 - i
|
|
);
|
|
}
|
|
assert_eq!(instances_checked, 3);
|
|
assert!(t.pop_smtp_status_update().await?.is_none());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_draft_and_send_webxdc_status_update() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
let alice_chat_id = alice.create_chat(&bob).await.id;
|
|
|
|
// prepare webxdc instance,
|
|
// status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None)
|
|
let mut alice_instance = create_webxdc_instance(
|
|
&alice,
|
|
"minimal.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
|
)
|
|
.await?;
|
|
alice_chat_id
|
|
.set_draft(&alice, Some(&mut alice_instance))
|
|
.await?;
|
|
let mut alice_instance = alice_chat_id.get_draft(&alice).await?.unwrap();
|
|
|
|
alice
|
|
.send_webxdc_status_update(alice_instance.id, r#"{"payload": {"foo":"bar"}}"#, "descr")
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
expect_status_update_event(&alice, alice_instance.id).await?;
|
|
alice
|
|
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#, "descr")
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
assert_eq!(
|
|
alice
|
|
.sql
|
|
.count("SELECT COUNT(*) FROM smtp_status_updates", ())
|
|
.await?,
|
|
0
|
|
);
|
|
assert!(!alice.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode
|
|
|
|
// send webxdc instance,
|
|
// the initial status updates are sent together in the same message
|
|
let alice_instance_id = send_msg(&alice, alice_chat_id, &mut alice_instance).await?;
|
|
let sent1 = alice.pop_sent_msg().await;
|
|
let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?;
|
|
assert_eq!(alice_instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(
|
|
alice_instance.get_filename(),
|
|
Some("minimal.xdc".to_string())
|
|
);
|
|
assert_eq!(alice_instance.chat_id, alice_chat_id);
|
|
|
|
// bob receives the instance together with the initial updates in a single message
|
|
let bob_instance = bob.recv_msg(&sent1).await;
|
|
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
|
assert_eq!(bob_instance.get_filename().unwrap(), "minimal.xdc");
|
|
assert!(sent1.payload().contains("Content-Type: application/json"));
|
|
assert!(sent1.payload().contains("status-update.json"));
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
|
|
{"payload":42,"info":"i","serial":2,"max_serial":2}]"#
|
|
);
|
|
assert!(!bob.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let msg_id = send_text_msg(&t, chat_id, "ho!".to_string()).await?;
|
|
assert!(t
|
|
.send_webxdc_status_update(msg_id, r#"{"foo":"bar"}"#, "descr")
|
|
.await
|
|
.is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_webxdc_blob() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
|
|
let buf = instance.get_webxdc_blob(&t, "index.html").await?;
|
|
assert_eq!(buf.len(), 188);
|
|
assert!(String::from_utf8_lossy(&buf).contains("document.write"));
|
|
|
|
assert!(instance
|
|
.get_webxdc_blob(&t, "not-existent.html")
|
|
.await
|
|
.is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_webxdc_blob_default_icon() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
|
|
let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?;
|
|
assert!(buf.len() > 100);
|
|
assert!(String::from_utf8_lossy(&buf).contains("PNG\r\n"));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
|
|
let buf = instance.get_webxdc_blob(&t, "/index.html").await?;
|
|
assert!(String::from_utf8_lossy(&buf).contains("document.write"));
|
|
|
|
assert!(instance.get_webxdc_blob(&t, "/not-there").await.is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"some-files.xdc",
|
|
include_bytes!("../test-data/webxdc/some-files.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
|
|
let buf = instance.get_webxdc_blob(&t, "index.html").await?;
|
|
assert_eq!(buf.len(), 65);
|
|
assert!(String::from_utf8_lossy(&buf).contains("many files"));
|
|
|
|
let buf = instance.get_webxdc_blob(&t, "subdir/bla.txt").await?;
|
|
assert_eq!(buf.len(), 4);
|
|
assert!(String::from_utf8_lossy(&buf).starts_with("bla"));
|
|
|
|
let buf = instance
|
|
.get_webxdc_blob(&t, "subdir/subsubdir/text.md")
|
|
.await?;
|
|
assert_eq!(buf.len(), 24);
|
|
assert!(String::from_utf8_lossy(&buf).starts_with("this is a markdown file"));
|
|
|
|
let buf = instance
|
|
.get_webxdc_blob(&t, "subdir/subsubdir/text2.md")
|
|
.await?;
|
|
assert_eq!(buf.len(), 22);
|
|
assert!(String::from_utf8_lossy(&buf).starts_with("another markdown"));
|
|
|
|
let buf = instance
|
|
.get_webxdc_blob(&t, "anotherdir/anothersubsubdir/foo.txt")
|
|
.await?;
|
|
assert_eq!(buf.len(), 4);
|
|
assert!(String::from_utf8_lossy(&buf).starts_with("foo"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parse_webxdc_manifest() -> Result<()> {
|
|
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());
|
|
assert!(result.is_err());
|
|
|
|
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?;
|
|
assert_eq!(manifest.name, None);
|
|
|
|
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?;
|
|
assert_eq!(manifest.name, Some("name, no icon".to_string()));
|
|
|
|
let manifest = parse_webxdc_manifest(
|
|
r#"name = "foo"
|
|
icon = "bar""#
|
|
.as_bytes(),
|
|
)?;
|
|
assert_eq!(manifest.name, Some("foo".to_string()));
|
|
|
|
let manifest = parse_webxdc_manifest(
|
|
r#"name = "foz"
|
|
icon = "baz"
|
|
add_item = "that should be just ignored"
|
|
|
|
[section]
|
|
sth_for_the = "future""#
|
|
.as_bytes(),
|
|
)?;
|
|
assert_eq!(manifest.name, Some("foz".to_string()));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
|
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?;
|
|
assert_eq!(manifest.min_api, Some(3));
|
|
|
|
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes());
|
|
assert!(result.is_err());
|
|
|
|
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes());
|
|
assert!(result.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
|
|
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes());
|
|
assert!(result.is_err());
|
|
|
|
let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?;
|
|
assert_eq!(
|
|
manifest.source_code_url,
|
|
Some("https://foo.bar".to_string())
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_parse_webxdc_manifest_request_internet_access() -> Result<()> {
|
|
let result = parse_webxdc_manifest(r#"request_internet_access = 3"#.as_bytes());
|
|
assert!(result.is_err());
|
|
let manifest = parse_webxdc_manifest(r#" source_code_url="https://foo.org""#.as_bytes())?;
|
|
assert_eq!(manifest.request_internet_access, None);
|
|
let manifest = parse_webxdc_manifest(r#" request_internet_access=false"#.as_bytes())?;
|
|
assert_eq!(manifest.request_internet_access, Some(false));
|
|
let manifest = parse_webxdc_manifest(r#"request_internet_access = true"#.as_bytes())?;
|
|
assert_eq!(manifest.request_internet_access, Some(true));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_min_api_too_large() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-min-api-1001.xdc",
|
|
include_bytes!("../test-data/webxdc/with-min-api-1001.xdc"),
|
|
)
|
|
.await?;
|
|
send_msg(&t, chat_id, &mut instance).await?;
|
|
|
|
let instance = t.get_last_msg().await;
|
|
let html = instance.get_webxdc_blob(&t, "index.html").await?;
|
|
assert!(String::from_utf8_lossy(&html).contains("requires a newer Delta Chat version"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_get_webxdc_info() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "minimal.xdc");
|
|
assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-manifest-empty-name.xdc",
|
|
include_bytes!("../test-data/webxdc/with-manifest-empty-name.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "with-manifest-empty-name.xdc");
|
|
assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-manifest-no-name.xdc",
|
|
include_bytes!("../test-data/webxdc/with-manifest-no-name.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "with-manifest-no-name.xdc");
|
|
assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-minimal-manifest.xdc",
|
|
include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "nice app!");
|
|
assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-manifest-and-png-icon.xdc",
|
|
include_bytes!("../test-data/webxdc/with-manifest-and-png-icon.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "with some icon");
|
|
assert_eq!(info.icon, "icon.png");
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-png-icon.xdc",
|
|
include_bytes!("../test-data/webxdc/with-png-icon.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "with-png-icon.xdc");
|
|
assert_eq!(info.icon, "icon.png");
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-jpg-icon.xdc",
|
|
include_bytes!("../test-data/webxdc/with-jpg-icon.xdc"),
|
|
)
|
|
.await?;
|
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "with-jpg-icon.xdc");
|
|
assert_eq!(info.icon, "icon.jpg");
|
|
|
|
let msg_id = send_text_msg(&t, chat_id, "foo".to_string()).await?;
|
|
let msg = Message::load_from_db(&t, msg_id).await?;
|
|
let result = msg.get_webxdc_info(&t).await;
|
|
assert!(result.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_info_summary() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Alice creates an webxdc instance and updates summary
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
let sent_instance = &alice.pop_sent_msg().await;
|
|
let info = alice_instance.get_webxdc_info(&alice).await?;
|
|
assert_eq!(info.summary, "".to_string());
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"summary":"sum: 1", "payload":1}"#,
|
|
"descr",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent_update1 = &alice.pop_sent_msg().await;
|
|
let info = Message::load_from_db(&alice, alice_instance.id)
|
|
.await?
|
|
.get_webxdc_info(&alice)
|
|
.await?;
|
|
assert_eq!(info.summary, "sum: 1".to_string());
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"summary":"sum: 2", "payload":2}"#,
|
|
"descr",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent_update2 = &alice.pop_sent_msg().await;
|
|
let info = Message::load_from_db(&alice, alice_instance.id)
|
|
.await?
|
|
.get_webxdc_info(&alice)
|
|
.await?;
|
|
assert_eq!(info.summary, "sum: 2".to_string());
|
|
|
|
// Bob receives the updates
|
|
let bob_instance = bob.recv_msg(sent_instance).await;
|
|
bob.recv_msg(sent_update1).await;
|
|
bob.recv_msg(sent_update2).await;
|
|
let info = Message::load_from_db(&bob, bob_instance.id)
|
|
.await?
|
|
.get_webxdc_info(&bob)
|
|
.await?;
|
|
assert_eq!(info.summary, "sum: 2".to_string());
|
|
|
|
// Alice has a second device and also receives the updates there
|
|
let alice2 = TestContext::new_alice().await;
|
|
let alice2_instance = alice2.recv_msg(sent_instance).await;
|
|
alice2.recv_msg(sent_update1).await;
|
|
alice2.recv_msg(sent_update2).await;
|
|
let info = Message::load_from_db(&alice2, alice2_instance.id)
|
|
.await?
|
|
.get_webxdc_info(&alice2)
|
|
.await?;
|
|
assert_eq!(info.summary, "sum: 2".to_string());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_document_name() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Alice creates an webxdc instance and updates document name
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
let sent_instance = &alice.pop_sent_msg().await;
|
|
let info = alice_instance.get_webxdc_info(&alice).await?;
|
|
assert_eq!(info.document, "".to_string());
|
|
assert_eq!(info.summary, "".to_string());
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"document":"my file", "payload":1337}"#,
|
|
"descr",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent_update1 = &alice.pop_sent_msg().await;
|
|
let info = Message::load_from_db(&alice, alice_instance.id)
|
|
.await?
|
|
.get_webxdc_info(&alice)
|
|
.await?;
|
|
assert_eq!(info.document, "my file".to_string());
|
|
assert_eq!(info.summary, "".to_string());
|
|
|
|
// Bob receives the updates
|
|
let bob_instance = bob.recv_msg(sent_instance).await;
|
|
bob.recv_msg(sent_update1).await;
|
|
let info = Message::load_from_db(&bob, bob_instance.id)
|
|
.await?
|
|
.get_webxdc_info(&bob)
|
|
.await?;
|
|
assert_eq!(info.document, "my file".to_string());
|
|
assert_eq!(info.summary, "".to_string());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_info_msg() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Alice sends update with an info message
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
let sent1 = &alice.pop_sent_msg().await;
|
|
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"info":"this appears in-chat", "payload":"sth. else"}"#,
|
|
"descr text",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent2 = &alice.pop_sent_msg().await;
|
|
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
|
let info_msg = alice.get_last_msg().await;
|
|
assert!(info_msg.is_info());
|
|
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
|
assert_eq!(info_msg.from_id, ContactId::SELF);
|
|
assert_eq!(info_msg.get_text(), "this appears in-chat");
|
|
assert_eq!(
|
|
info_msg.parent(&alice).await?.unwrap().id,
|
|
alice_instance.id
|
|
);
|
|
assert!(info_msg.quoted_message(&alice).await?.is_none());
|
|
assert_eq!(
|
|
alice
|
|
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
// Bob receives all messages
|
|
let bob_instance = bob.recv_msg(sent1).await;
|
|
let bob_chat_id = bob_instance.chat_id;
|
|
bob.recv_msg(sent2).await;
|
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
|
let info_msg = bob.get_last_msg().await;
|
|
assert!(info_msg.is_info());
|
|
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
|
assert!(!info_msg.from_id.is_special());
|
|
assert_eq!(info_msg.get_text(), "this appears in-chat");
|
|
assert_eq!(info_msg.parent(&bob).await?.unwrap().id, bob_instance.id);
|
|
assert!(info_msg.quoted_message(&bob).await?.is_none());
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
// Alice has a second device and also receives the info message there
|
|
let alice2 = TestContext::new_alice().await;
|
|
let alice2_instance = alice2.recv_msg(sent1).await;
|
|
let alice2_chat_id = alice2_instance.chat_id;
|
|
alice2.recv_msg(sent2).await;
|
|
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2);
|
|
let info_msg = alice2.get_last_msg().await;
|
|
assert!(info_msg.is_info());
|
|
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
|
assert_eq!(info_msg.from_id, ContactId::SELF);
|
|
assert_eq!(info_msg.get_text(), "this appears in-chat");
|
|
assert_eq!(
|
|
info_msg.parent(&alice2).await?.unwrap().id,
|
|
alice2_instance.id
|
|
);
|
|
assert!(info_msg.quoted_message(&alice2).await?.is_none());
|
|
assert_eq!(
|
|
alice2
|
|
.get_webxdc_status_updates(alice2_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
let sent1 = &alice.pop_sent_msg().await;
|
|
|
|
// Alice sends two info messages in a row;
|
|
// the second one removes the first one as there is nothing in between
|
|
alice
|
|
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i1", "payload":1}"#, "d")
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent2 = &alice.pop_sent_msg().await;
|
|
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
|
alice
|
|
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#, "d")
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent3 = &alice.pop_sent_msg().await;
|
|
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
|
let info_msg = alice.get_last_msg().await;
|
|
assert_eq!(info_msg.get_text(), "i2");
|
|
|
|
// When Bob receives the messages, they should be cleaned up as well
|
|
let bob_instance = bob.recv_msg(sent1).await;
|
|
let bob_chat_id = bob_instance.chat_id;
|
|
bob.recv_msg(sent2).await;
|
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
|
bob.recv_msg(sent3).await;
|
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
|
let info_msg = bob.get_last_msg().await;
|
|
assert_eq!(info_msg.get_text(), "i2");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?;
|
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
|
|
|
t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#, "d")
|
|
.await?;
|
|
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2);
|
|
send_text_msg(&t, chat_id, "msg between info".to_string()).await?;
|
|
assert_eq!(chat_id.get_msg_cnt(&t).await?, 3);
|
|
t.send_webxdc_status_update(instance.id, r#"{"info":"i2", "payload":2}"#, "d")
|
|
.await?;
|
|
assert_eq!(chat_id.get_msg_cnt(&t).await?, 4);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_opportunistic_encryption() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Bob sends sth. to Alice, Alice has Bob's key
|
|
let bob_chat_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "chat").await?;
|
|
add_contact_to_chat(
|
|
&bob,
|
|
bob_chat_id,
|
|
Contact::create(&bob, "", "alice@example.org").await?,
|
|
)
|
|
.await?;
|
|
send_text_msg(&bob, bob_chat_id, "populate".to_string()).await?;
|
|
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
|
|
|
// Alice sends instance+update to Bob
|
|
let alice_chat_id = alice.get_last_msg().await.chat_id;
|
|
alice_chat_id.accept(&alice).await?;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat_id).await?;
|
|
let sent1 = &alice.pop_sent_msg().await;
|
|
alice
|
|
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42}"#, "descr")
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
let sent2 = &alice.pop_sent_msg().await;
|
|
let update_msg = sent2.load_from_db().await;
|
|
assert!(alice_instance.get_showpadlock());
|
|
assert!(update_msg.get_showpadlock());
|
|
|
|
// Bob receives instance+update
|
|
let bob_instance = bob.recv_msg(sent1).await;
|
|
bob.recv_msg(sent2).await;
|
|
assert!(bob_instance.get_showpadlock());
|
|
|
|
// Bob adds Claire with unknown key, update to Alice+Claire cannot be encrypted
|
|
add_contact_to_chat(
|
|
&bob,
|
|
bob_chat_id,
|
|
Contact::create(&bob, "", "claire@example.org").await?,
|
|
)
|
|
.await?;
|
|
bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":43}"#, "descr")
|
|
.await?;
|
|
bob.flush_status_updates().await?;
|
|
let sent3 = bob.pop_sent_msg().await;
|
|
let update_msg = sent3.load_from_db().await;
|
|
assert!(!update_msg.get_showpadlock());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_internet_access() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let self_id = t.get_self_chat().await.id;
|
|
let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id;
|
|
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
|
let broadcast_id = create_broadcast_list(&t).await?;
|
|
|
|
let mut first_test = true; // only the first test has all conditions for internet access
|
|
|
|
for e2ee in ["1", "0"] {
|
|
t.set_config(Config::E2eeEnabled, Some(e2ee)).await?;
|
|
for chat_id in [self_id, single_id, group_id, broadcast_id] {
|
|
for internet_xdc in [true, false] {
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"foo.xdc",
|
|
if internet_xdc {
|
|
include_bytes!("../test-data/webxdc/request-internet-access.xdc")
|
|
} else {
|
|
include_bytes!("../test-data/webxdc/minimal.xdc")
|
|
},
|
|
)
|
|
.await?;
|
|
let instance_id = send_msg(&t, chat_id, &mut instance).await?;
|
|
t.send_webxdc_status_update(
|
|
instance_id,
|
|
r#"{"summary":"real summary", "payload": 42}"#,
|
|
"descr",
|
|
)
|
|
.await?;
|
|
let instance = Message::load_from_db(&t, instance_id).await?;
|
|
let info = instance.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.internet_access, first_test);
|
|
assert_eq!(info.summary.contains("Do not enter sensitive"), first_test);
|
|
assert_eq!(info.summary.contains("real summary"), !first_test);
|
|
|
|
first_test = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_chatlist_summary() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
|
let mut instance = create_webxdc_instance(
|
|
&t,
|
|
"with-minimal-manifest.xdc",
|
|
include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
|
|
)
|
|
.await?;
|
|
send_msg(&t, chat_id, &mut instance).await?;
|
|
|
|
let chatlist = Chatlist::try_load(&t, 0, None, None).await?;
|
|
assert_eq!(chatlist.len(), 1);
|
|
let summary = chatlist.get_summary(&t, 0, None).await?;
|
|
assert_eq!(summary.text, "nice app!".to_string());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_and_text() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
// Alice sends instance and adds some text
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let mut alice_instance = create_webxdc_instance(
|
|
&alice,
|
|
"minimal.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
|
)
|
|
.await?;
|
|
alice_instance.set_text("user added text".to_string());
|
|
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
|
|
let alice_instance = alice.get_last_msg().await;
|
|
assert_eq!(alice_instance.get_text(), "user added text");
|
|
|
|
// Bob receives that instance
|
|
let sent1 = alice.pop_sent_msg().await;
|
|
let bob_instance = bob.recv_msg(&sent1).await;
|
|
assert_eq!(bob_instance.get_text(), "user added text");
|
|
|
|
// Alice's second device receives the instance as well
|
|
let alice2 = TestContext::new_alice().await;
|
|
let alice2_instance = alice2.recv_msg(&sent1).await;
|
|
assert_eq!(alice2_instance.get_text(), "user added text");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn helper_send_receive_status_update(
|
|
bob: &TestContext,
|
|
alice: &TestContext,
|
|
bob_instance: &Message,
|
|
alice_instance: &Message,
|
|
) -> Result<String> {
|
|
bob.send_webxdc_status_update(
|
|
bob_instance.id,
|
|
r#"{"payload":7,"info": "i","summary":"s"}"#,
|
|
"",
|
|
)
|
|
.await?;
|
|
bob.flush_status_updates().await?;
|
|
let msg = bob.pop_sent_msg().await;
|
|
alice.recv_msg(&msg).await;
|
|
alice
|
|
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
|
|
.await
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
|
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
|
add_contact_to_chat(&alice, chat_id, contact_bob).await?;
|
|
let instance = send_webxdc_instance(&alice, chat_id).await?;
|
|
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
|
let bob_instance = bob.get_last_msg().await;
|
|
Chat::load_from_db(&bob, bob_instance.chat_id)
|
|
.await?
|
|
.id
|
|
.accept(&bob)
|
|
.await?;
|
|
|
|
let status =
|
|
helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?;
|
|
assert_eq!(
|
|
status,
|
|
r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
remove_contact_from_chat(&alice, chat_id, contact_bob).await?;
|
|
let status =
|
|
helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?;
|
|
|
|
assert_eq!(
|
|
status,
|
|
r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"#
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_webxdc_delete_event() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
|
let instance = send_webxdc_instance(&alice, chat_id).await?;
|
|
message::delete_msgs(&alice, &[instance.id]).await?;
|
|
alice
|
|
.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::WebxdcInstanceDeleted { .. }))
|
|
.await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn change_logging_webxdc() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
|
|
|
assert_eq!(
|
|
alice
|
|
.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?,
|
|
0
|
|
);
|
|
|
|
let mut instance = create_webxdc_instance(
|
|
&alice,
|
|
"debug_logging.xdc",
|
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
|
)
|
|
.await?;
|
|
assert!(alice.debug_logging.read().unwrap().is_none());
|
|
send_msg(&alice, chat_id, &mut instance).await?;
|
|
assert!(alice.debug_logging.read().unwrap().is_some());
|
|
|
|
alice.emit_event(EventType::Info("hi".to_string()));
|
|
alice
|
|
.evtracker
|
|
.get_matching(|ev| matches!(*ev, EventType::WebxdcStatusUpdate { .. }))
|
|
.await;
|
|
assert!(
|
|
alice
|
|
.sql
|
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", ())
|
|
.await?
|
|
> 0
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests extensibility of WebXDC updates.
|
|
///
|
|
/// If an update sent by WebXDC contains unknown properties,
|
|
/// such as `aNewUnknownProperty` or a reserved property
|
|
/// like `serial` or `max_serial`,
|
|
/// they are silently dropped and are not sent over the wire.
|
|
///
|
|
/// This ensures new WebXDC can try to send new properties
|
|
/// added in later revisions of the WebXDC API
|
|
/// and this will not result in a failure to send the whole update.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_webxdc_status_update_extensibility() -> Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
let alice_chat = alice.create_chat(&bob).await;
|
|
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
|
|
|
let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
|
|
|
alice
|
|
.send_webxdc_status_update(
|
|
alice_instance.id,
|
|
r#"{"payload":"p","info":"i","aNewUnknownProperty":"x","max_serial":123}"#,
|
|
"Some description",
|
|
)
|
|
.await?;
|
|
alice.flush_status_updates().await?;
|
|
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
|
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":"p","info":"i","serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_status_update_vs_delete_device_after() -> Result<()> {
|
|
let alice = &TestContext::new_alice().await;
|
|
let bob = &TestContext::new_bob().await;
|
|
bob.set_config(Config::DeleteDeviceAfter, Some("3600"))
|
|
.await?;
|
|
let alice_chat = alice.create_chat(bob).await;
|
|
let alice_instance = send_webxdc_instance(alice, alice_chat.id).await?;
|
|
let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
|
|
|
SystemTime::shift(Duration::from_secs(1800)).await;
|
|
let mut update = Message {
|
|
chat_id: alice_chat.id,
|
|
viewtype: Viewtype::Text,
|
|
text: "I'm an update".to_string(),
|
|
hidden: true,
|
|
..Default::default()
|
|
};
|
|
update.param.set_cmd(SystemMessage::WebxdcStatusUpdate);
|
|
update
|
|
.param
|
|
.set(Param::Arg, r#"{"updates":[{"payload":{"foo":"bar"}}]}"#);
|
|
update.set_quote(alice, Some(&alice_instance)).await?;
|
|
let sent_msg = alice.send_msg(alice_chat.id, &mut update).await;
|
|
bob.recv_msg(&sent_msg).await;
|
|
assert_eq!(
|
|
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
|
.await?,
|
|
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
|
|
);
|
|
|
|
SystemTime::shift(Duration::from_secs(2700)).await;
|
|
ephemeral::delete_expired_messages(bob, tools::time()).await?;
|
|
let bob_instance = Message::load_from_db(bob, bob_instance.id).await?;
|
|
assert_eq!(bob_instance.chat_id.is_trash(), false);
|
|
|
|
Ok(())
|
|
}
|
|
}
|