mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
basic w30 sending and receiving
This commit is contained in:
@@ -37,6 +37,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
|
|||||||
use deltachat::key::DcKey;
|
use deltachat::key::DcKey;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
|
use deltachat::w30::StatusUpdateId;
|
||||||
use deltachat::*;
|
use deltachat::*;
|
||||||
use deltachat::{accounts::Accounts, log::LogExt};
|
use deltachat::{accounts::Accounts, log::LogExt};
|
||||||
|
|
||||||
@@ -500,6 +501,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
EventType::ImexFileWritten(_) => 0,
|
EventType::ImexFileWritten(_) => 0,
|
||||||
EventType::SecurejoinInviterProgress { contact_id, .. }
|
EventType::SecurejoinInviterProgress { contact_id, .. }
|
||||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
||||||
|
EventType::W30StatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,6 +543,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
EventType::SecurejoinInviterProgress { progress, .. }
|
EventType::SecurejoinInviterProgress { progress, .. }
|
||||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||||
|
EventType::W30StatusUpdate {
|
||||||
|
status_update_id, ..
|
||||||
|
} => status_update_id.to_u32() as libc::c_int,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,6 +587,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
|||||||
| EventType::SecurejoinJoinerProgress { .. }
|
| EventType::SecurejoinJoinerProgress { .. }
|
||||||
| EventType::ConnectivityChanged
|
| EventType::ConnectivityChanged
|
||||||
| EventType::SelfavatarChanged
|
| EventType::SelfavatarChanged
|
||||||
|
| EventType::W30StatusUpdate { .. }
|
||||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||||
EventType::ConfigureProgress { comment, .. } => {
|
EventType::ConfigureProgress { comment, .. } => {
|
||||||
if let Some(comment) = comment {
|
if let Some(comment) = comment {
|
||||||
@@ -871,6 +877,52 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_send_w30_status_update(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
msg_id: u32,
|
||||||
|
descr: *const libc::c_char,
|
||||||
|
json: *const libc::c_char,
|
||||||
|
) -> libc::c_int {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_send_w30_status_update()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
|
||||||
|
block_on(ctx.send_w30_status_update(
|
||||||
|
MsgId::new(msg_id),
|
||||||
|
&to_string_lossy(descr),
|
||||||
|
&to_string_lossy(json),
|
||||||
|
))
|
||||||
|
.log_err(ctx, "Failed to send w30 update")
|
||||||
|
.is_ok() as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_get_w30_status_updates(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
msg_id: u32,
|
||||||
|
status_update_id: u32,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_get_w30_status_updates()");
|
||||||
|
return "".strdup();
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
|
||||||
|
block_on(ctx.get_w30_status_updates(
|
||||||
|
MsgId::new(msg_id),
|
||||||
|
if status_update_id == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(StatusUpdateId::new(status_update_id))
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap_or_else(|_| "".to_string())
|
||||||
|
.strdup()
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_set_draft(
|
pub unsafe extern "C" fn dc_set_draft(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
sendfile <file> [<text>]\n\
|
sendfile <file> [<text>]\n\
|
||||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||||
sendsyncmsg\n\
|
sendsyncmsg\n\
|
||||||
|
sendw30 <msg-id> <json status update>\n\
|
||||||
videochat\n\
|
videochat\n\
|
||||||
draft [<text>]\n\
|
draft [<text>]\n\
|
||||||
devicemsg <text>\n\
|
devicemsg <text>\n\
|
||||||
@@ -907,6 +908,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||||
None => println!("sync message not needed."),
|
None => println!("sync message not needed."),
|
||||||
},
|
},
|
||||||
|
"sendw30" => {
|
||||||
|
ensure!(
|
||||||
|
!arg1.is_empty() && !arg2.is_empty(),
|
||||||
|
"Arguments <msg-id> <json status update> expected"
|
||||||
|
);
|
||||||
|
let msg_id = MsgId::new(arg1.parse()?);
|
||||||
|
context
|
||||||
|
.send_w30_status_update(msg_id, "this is a w30 status update", arg2)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
"videochat" => {
|
"videochat" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const DB_COMMANDS: [&str; 10] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 35] = [
|
const CHAT_COMMANDS: [&str; 36] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"chat",
|
"chat",
|
||||||
@@ -191,6 +191,7 @@ const CHAT_COMMANDS: [&str; 35] = [
|
|||||||
"sendfile",
|
"sendfile",
|
||||||
"sendhtml",
|
"sendhtml",
|
||||||
"sendsyncmsg",
|
"sendsyncmsg",
|
||||||
|
"sendw30",
|
||||||
"videochat",
|
"videochat",
|
||||||
"draft",
|
"draft",
|
||||||
"listmedia",
|
"listmedia",
|
||||||
|
|||||||
10
src/chat.rs
10
src/chat.rs
@@ -37,6 +37,7 @@ use crate::mimeparser::SystemMessage;
|
|||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
|
use crate::w30::W30_SUFFIX;
|
||||||
|
|
||||||
/// An chat item, such as a message or a marker.
|
/// An chat item, such as a message or a marker.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
@@ -645,6 +646,9 @@ impl ChatId {
|
|||||||
.await?
|
.await?
|
||||||
.context("no file stored in params")?;
|
.context("no file stored in params")?;
|
||||||
msg.param.set(Param::File, blob.as_name());
|
msg.param.set(Param::File, blob.as_name());
|
||||||
|
if blob.suffix() == Some(W30_SUFFIX) {
|
||||||
|
msg.viewtype = Viewtype::W30;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1796,6 +1800,7 @@ pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
|
|||||||
Viewtype::Video => true,
|
Viewtype::Video => true,
|
||||||
Viewtype::File => true,
|
Viewtype::File => true,
|
||||||
Viewtype::VideochatInvitation => false,
|
Viewtype::VideochatInvitation => false,
|
||||||
|
Viewtype::W30 => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1836,6 +1841,11 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
|||||||
msg.param.set(Param::MimeType, mime);
|
msg.param.set(Param::MimeType, mime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.viewtype == Viewtype::W30 && blob.suffix() != Some(W30_SUFFIX) {
|
||||||
|
bail!("w30 message {} does not have suffix {}", blob, W30_SUFFIX);
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"Attaching \"{}\" for message type #{}.",
|
"Attaching \"{}\" for message type #{}.",
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ pub enum Viewtype {
|
|||||||
|
|
||||||
/// Message is an invitation to a videochat.
|
/// Message is an invitation to a videochat.
|
||||||
VideochatInvitation = 70,
|
VideochatInvitation = 70,
|
||||||
|
|
||||||
|
/// Message is an w30 object.
|
||||||
|
W30 = 80,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Viewtype {
|
impl Default for Viewtype {
|
||||||
@@ -339,6 +342,7 @@ mod tests {
|
|||||||
Viewtype::VideochatInvitation,
|
Viewtype::VideochatInvitation,
|
||||||
Viewtype::from_i32(70).unwrap()
|
Viewtype::from_i32(70).unwrap()
|
||||||
);
|
);
|
||||||
|
assert_eq!(Viewtype::W30, Viewtype::from_i32(80).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -258,6 +258,15 @@ pub(crate) async fn dc_receive_imf_inner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref status_update) = mime_parser.w30_status_update {
|
||||||
|
if let Err(err) = context
|
||||||
|
.receive_status_update(insert_msg_id, status_update)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(context, "receive_imf cannot update status: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||||
if from_id != 0
|
if from_id != 0
|
||||||
&& context
|
&& context
|
||||||
@@ -860,6 +869,15 @@ async fn add_parts(
|
|||||||
info!(context, "Existing non-decipherable message. (TRASH)");
|
info!(context, "Existing non-decipherable message. (TRASH)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mime_parser.w30_status_update.is_some() && mime_parser.parts.len() == 1 {
|
||||||
|
if let Some(part) = mime_parser.parts.first() {
|
||||||
|
if part.typ == Viewtype::Text && part.msg.is_empty() {
|
||||||
|
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||||
|
info!(context, "Message is a status update only (TRASH)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if is_mdn {
|
if is_mdn {
|
||||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use strum::EnumProperty;
|
|||||||
use crate::chat::ChatId;
|
use crate::chat::ChatId;
|
||||||
use crate::ephemeral::Timer as EphemeralTimer;
|
use crate::ephemeral::Timer as EphemeralTimer;
|
||||||
use crate::message::MsgId;
|
use crate::message::MsgId;
|
||||||
|
use crate::w30::StatusUpdateId;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Events {
|
pub struct Events {
|
||||||
@@ -326,4 +327,10 @@ pub enum EventType {
|
|||||||
|
|
||||||
#[strum(props(id = "2110"))]
|
#[strum(props(id = "2110"))]
|
||||||
SelfavatarChanged,
|
SelfavatarChanged,
|
||||||
|
|
||||||
|
#[strum(props(id = "2120"))]
|
||||||
|
W30StatusUpdate {
|
||||||
|
msg_id: MsgId,
|
||||||
|
status_update_id: StatusUpdateId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ pub mod stock_str;
|
|||||||
mod sync;
|
mod sync;
|
||||||
mod token;
|
mod token;
|
||||||
mod update_helper;
|
mod update_helper;
|
||||||
|
pub mod w30;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod dehtml;
|
mod dehtml;
|
||||||
mod color;
|
mod color;
|
||||||
|
|||||||
@@ -121,6 +121,13 @@ WHERE id=?;
|
|||||||
.sql
|
.sql
|
||||||
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self])
|
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self])
|
||||||
.await?;
|
.await?;
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"DELETE FROM msgs_status_updates WHERE msg_id=?;",
|
||||||
|
paramsv![self],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
|
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
|
||||||
@@ -1161,6 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
|||||||
"ttf" => (Viewtype::File, "font/ttf"),
|
"ttf" => (Viewtype::File, "font/ttf"),
|
||||||
"vcard" => (Viewtype::File, "text/vcard"),
|
"vcard" => (Viewtype::File, "text/vcard"),
|
||||||
"vcf" => (Viewtype::File, "text/vcard"),
|
"vcf" => (Viewtype::File, "text/vcard"),
|
||||||
|
"w30" => (Viewtype::W30, "application/html+w30"),
|
||||||
"wav" => (Viewtype::File, "audio/wav"),
|
"wav" => (Viewtype::File, "audio/wav"),
|
||||||
"weba" => (Viewtype::File, "audio/webm"),
|
"weba" => (Viewtype::File, "audio/webm"),
|
||||||
"webm" => (Viewtype::Video, "video/webm"),
|
"webm" => (Viewtype::Video, "video/webm"),
|
||||||
@@ -1691,6 +1699,14 @@ mod tests {
|
|||||||
guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
|
guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
|
||||||
Some((Viewtype::Audio, "audio/mpeg"))
|
Some((Viewtype::Audio, "audio/mpeg"))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
guess_msgtype_from_suffix(Path::new("foo/file.html")),
|
||||||
|
Some((Viewtype::File, "text/html"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
guess_msgtype_from_suffix(Path::new("foo/file.w30")),
|
||||||
|
Some((Viewtype::W30, "application/html+w30"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
|
|||||||
@@ -641,6 +641,11 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"Content-Type".to_string(),
|
"Content-Type".to_string(),
|
||||||
"multipart/report; report-type=multi-device-sync".to_string(),
|
"multipart/report; report-type=multi-device-sync".to_string(),
|
||||||
))
|
))
|
||||||
|
} else if self.msg.param.get_cmd() == SystemMessage::W30StatusUpdate {
|
||||||
|
PartBuilder::new().header((
|
||||||
|
"Content-Type".to_string(),
|
||||||
|
"multipart/report; report-type=status-update".to_string(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
PartBuilder::new().message_type(MimeMultipartType::Mixed)
|
PartBuilder::new().message_type(MimeMultipartType::Mixed)
|
||||||
};
|
};
|
||||||
@@ -915,7 +920,9 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"ephemeral-timer-changed".to_string(),
|
"ephemeral-timer-changed".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
|
SystemMessage::LocationOnly
|
||||||
|
| SystemMessage::MultiDeviceSync
|
||||||
|
| SystemMessage::W30StatusUpdate => {
|
||||||
// This should prevent automatic replies,
|
// This should prevent automatic replies,
|
||||||
// such as non-delivery reports.
|
// such as non-delivery reports.
|
||||||
//
|
//
|
||||||
@@ -1152,6 +1159,14 @@ impl<'a> MimeFactory<'a> {
|
|||||||
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
||||||
parts.push(context.build_sync_part(json.to_string()).await);
|
parts.push(context.build_sync_part(json.to_string()).await);
|
||||||
self.sync_ids_to_delete = Some(ids.to_string());
|
self.sync_ids_to_delete = Some(ids.to_string());
|
||||||
|
} else if command == SystemMessage::W30StatusUpdate {
|
||||||
|
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||||
|
parts.push(context.build_status_update_part(json).await);
|
||||||
|
} else if self.msg.viewtype == Viewtype::W30 {
|
||||||
|
let json = context.get_w30_status_updates(self.msg.id, None).await?;
|
||||||
|
if json != "[]" {
|
||||||
|
parts.push(context.build_status_update_part(&json).await);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.attach_selfavatar {
|
if self.attach_selfavatar {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ pub struct MimeMessage {
|
|||||||
pub location_kml: Option<location::Kml>,
|
pub location_kml: Option<location::Kml>,
|
||||||
pub message_kml: Option<location::Kml>,
|
pub message_kml: Option<location::Kml>,
|
||||||
pub(crate) sync_items: Option<SyncItems>,
|
pub(crate) sync_items: Option<SyncItems>,
|
||||||
|
pub(crate) w30_status_update: Option<String>,
|
||||||
pub(crate) user_avatar: Option<AvatarAction>,
|
pub(crate) user_avatar: Option<AvatarAction>,
|
||||||
pub(crate) group_avatar: Option<AvatarAction>,
|
pub(crate) group_avatar: Option<AvatarAction>,
|
||||||
pub(crate) mdn_reports: Vec<Report>,
|
pub(crate) mdn_reports: Vec<Report>,
|
||||||
@@ -135,6 +136,10 @@ pub enum SystemMessage {
|
|||||||
/// Self-sent-message that contains only json used for multi-device-sync;
|
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||||
/// if possible, we attach that to other messages as for locations.
|
/// if possible, we attach that to other messages as for locations.
|
||||||
MultiDeviceSync = 20,
|
MultiDeviceSync = 20,
|
||||||
|
|
||||||
|
// Sync message that contains a json payload
|
||||||
|
// sent to the other w30 instances
|
||||||
|
W30StatusUpdate = 30,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SystemMessage {
|
impl Default for SystemMessage {
|
||||||
@@ -296,6 +301,7 @@ impl MimeMessage {
|
|||||||
location_kml: None,
|
location_kml: None,
|
||||||
message_kml: None,
|
message_kml: None,
|
||||||
sync_items: None,
|
sync_items: None,
|
||||||
|
w30_status_update: None,
|
||||||
user_avatar: None,
|
user_avatar: None,
|
||||||
group_avatar: None,
|
group_avatar: None,
|
||||||
failure_report: None,
|
failure_report: None,
|
||||||
@@ -538,7 +544,7 @@ impl MimeMessage {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref subject) = self.get_subject() {
|
if let Some(ref subject) = self.get_subject() {
|
||||||
if !self.has_chat_version() {
|
if !self.has_chat_version() && self.w30_status_update.is_none() {
|
||||||
part.msg = subject.to_string();
|
part.msg = subject.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -837,6 +843,12 @@ impl MimeMessage {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some("status-update") => {
|
||||||
|
if let Some(second) = mail.subparts.get(1) {
|
||||||
|
self.add_single_part_if_known(context, second, is_related)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
if let Some(first) = mail.subparts.get(0) {
|
if let Some(first) = mail.subparts.get(0) {
|
||||||
any_part_added = self
|
any_part_added = self
|
||||||
@@ -1006,8 +1018,13 @@ impl MimeMessage {
|
|||||||
if decoded_data.is_empty() {
|
if decoded_data.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// treat location/message kml file attachments specially
|
let msg_type = if context
|
||||||
if filename.ends_with(".kml") {
|
.is_w30_file(filename, decoded_data)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
Viewtype::W30
|
||||||
|
} else if filename.ends_with(".kml") {
|
||||||
// XXX what if somebody sends eg an "location-highlights.kml"
|
// XXX what if somebody sends eg an "location-highlights.kml"
|
||||||
// attachment unrelated to location streaming?
|
// attachment unrelated to location streaming?
|
||||||
if filename.starts_with("location") || filename.starts_with("message") {
|
if filename.starts_with("location") || filename.starts_with("message") {
|
||||||
@@ -1023,6 +1040,7 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
msg_type
|
||||||
} else if filename == "multi-device-sync.json" {
|
} else if filename == "multi-device-sync.json" {
|
||||||
let serialized = String::from_utf8_lossy(decoded_data)
|
let serialized = String::from_utf8_lossy(decoded_data)
|
||||||
.parse()
|
.parse()
|
||||||
@@ -1035,7 +1053,15 @@ impl MimeMessage {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
return;
|
return;
|
||||||
}
|
} else if filename == "status-update.json" {
|
||||||
|
let serialized = String::from_utf8_lossy(decoded_data)
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.w30_status_update = Some(serialized);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
msg_type
|
||||||
|
};
|
||||||
|
|
||||||
/* we have a regular file attachment,
|
/* we have a regular file attachment,
|
||||||
write decoded data to new blob object */
|
write decoded data to new blob object */
|
||||||
|
|||||||
@@ -137,7 +137,11 @@ impl Message {
|
|||||||
append_text = false;
|
append_text = false;
|
||||||
stock_str::videochat_invitation(context).await
|
stock_str::videochat_invitation(context).await
|
||||||
}
|
}
|
||||||
_ => {
|
Viewtype::W30 => {
|
||||||
|
append_text = true;
|
||||||
|
"W30".to_string()
|
||||||
|
}
|
||||||
|
Viewtype::Text | Viewtype::Unknown => {
|
||||||
if self.param.get_cmd() != SystemMessage::LocationOnly {
|
if self.param.get_cmd() != SystemMessage::LocationOnly {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
594
src/w30.rs
Normal file
594
src/w30.rs
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
//! # Handle W30 messages.
|
||||||
|
|
||||||
|
use crate::constants::Viewtype;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::message::{Message, MessageState, MsgId};
|
||||||
|
use crate::mimeparser::SystemMessage;
|
||||||
|
use crate::param::Param;
|
||||||
|
use crate::{chat, EventType};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use lettre_email::mime::{self};
|
||||||
|
use lettre_email::PartBuilder;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
pub const W30_SUFFIX: &str = "w30";
|
||||||
|
|
||||||
|
/// Status Update ID.
|
||||||
|
#[derive(
|
||||||
|
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct StatusUpdateId(u32);
|
||||||
|
|
||||||
|
impl StatusUpdateId {
|
||||||
|
/// Create a new [MsgId].
|
||||||
|
pub fn new(id: u32) -> StatusUpdateId {
|
||||||
|
StatusUpdateId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets StatusUpdateId as untyped integer.
|
||||||
|
/// Avoid using this outside ffi.
|
||||||
|
pub fn to_u32(self) -> u32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rusqlite::types::ToSql for StatusUpdateId {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
|
let val = rusqlite::types::Value::Integer(self.0 as i64);
|
||||||
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct StatusUpdateItem {
|
||||||
|
payload: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub(crate) async fn is_w30_file(&self, filename: &str, _decoded_data: &[u8]) -> Result<bool> {
|
||||||
|
if filename.ends_with(W30_SUFFIX) {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_status_update_record(
|
||||||
|
&self,
|
||||||
|
instance_msg_id: MsgId,
|
||||||
|
payload: &str,
|
||||||
|
) -> Result<StatusUpdateId> {
|
||||||
|
let payload = payload.trim();
|
||||||
|
if payload.is_empty() {
|
||||||
|
bail!("create_status_update_record: empty payload");
|
||||||
|
}
|
||||||
|
let _test: Value = serde_json::from_str(payload)?; // checks if input data are valid json
|
||||||
|
|
||||||
|
let rowid = self
|
||||||
|
.sql
|
||||||
|
.insert(
|
||||||
|
"INSERT INTO msgs_status_updates (msg_id, payload) VALUES(?, ?);",
|
||||||
|
paramsv![instance_msg_id, payload],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusUpdateId(u32::try_from(rowid)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a status update for an w30 instance.
|
||||||
|
///
|
||||||
|
/// If the instance is a draft,
|
||||||
|
/// the status update is sent once the instance is actually sent.
|
||||||
|
///
|
||||||
|
/// If an update is sent immediately, the message-id of the update-message is returned,
|
||||||
|
/// this update-message is visible in chats, however, the id may be useful.
|
||||||
|
pub async fn send_w30_status_update(
|
||||||
|
&self,
|
||||||
|
instance_msg_id: MsgId,
|
||||||
|
descr: &str,
|
||||||
|
payload: &str,
|
||||||
|
) -> Result<Option<MsgId>> {
|
||||||
|
let instance = Message::load_from_db(self, instance_msg_id).await?;
|
||||||
|
if instance.viewtype != Viewtype::W30 {
|
||||||
|
bail!("send_w30_status_update: is no w30 message");
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_update_id = self
|
||||||
|
.create_status_update_record(instance_msg_id, payload)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match instance.state {
|
||||||
|
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft => {
|
||||||
|
// send update once the instance is actually send;
|
||||||
|
// on sending, the updates are retrieved using get_w30_status_updates() then.
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// send update now
|
||||||
|
// (also send updates on MessagesState::Failed, maybe only one member cannot receive)
|
||||||
|
let mut status_update = Message {
|
||||||
|
chat_id: instance.chat_id,
|
||||||
|
viewtype: Viewtype::Text,
|
||||||
|
text: Some(descr.to_string()),
|
||||||
|
hidden: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
status_update.param.set_cmd(SystemMessage::W30StatusUpdate);
|
||||||
|
status_update.param.set(
|
||||||
|
Param::Arg,
|
||||||
|
self.get_w30_status_updates(instance_msg_id, Some(status_update_id))
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
status_update.set_quote(self, &instance).await?;
|
||||||
|
let status_update_msg_id =
|
||||||
|
chat::send_msg(self, instance.chat_id, &mut status_update).await?;
|
||||||
|
Ok(Some(status_update_msg_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn build_status_update_part(&self, json: &str) -> PartBuilder {
|
||||||
|
PartBuilder::new()
|
||||||
|
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
||||||
|
.header((
|
||||||
|
"Content-Disposition",
|
||||||
|
"attachment; filename=\"status-update.json\"",
|
||||||
|
))
|
||||||
|
.body(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives status updates from receive_imf to the database
|
||||||
|
/// and sends out an event.
|
||||||
|
///
|
||||||
|
/// `msg_id` may be an instance (in case there are initial status updates)
|
||||||
|
/// or a reply to an instance (for all other updates).
|
||||||
|
///
|
||||||
|
/// `json_array` is an array containing one or more payloads as created by send_w30_status_update(),
|
||||||
|
/// the array is parsed using serde, the single payloads are used as is.
|
||||||
|
pub(crate) async fn receive_status_update(
|
||||||
|
&self,
|
||||||
|
msg_id: MsgId,
|
||||||
|
json_array: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let msg = Message::load_from_db(self, msg_id).await?;
|
||||||
|
let instance = if msg.viewtype == Viewtype::W30 {
|
||||||
|
msg
|
||||||
|
} else if let Some(parent) = msg.parent(self).await? {
|
||||||
|
if parent.viewtype == Viewtype::W30 {
|
||||||
|
parent
|
||||||
|
} else {
|
||||||
|
bail!("receive_status_update: message is not the child of a W30 message.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("receive_status_update: status message has no parent.")
|
||||||
|
};
|
||||||
|
|
||||||
|
let payloads: Vec<Value> = serde_json::from_str(json_array)?;
|
||||||
|
for payload in payloads {
|
||||||
|
let status_update_id = self
|
||||||
|
.create_status_update_record(instance.id, &*serde_json::to_string(&payload)?)
|
||||||
|
.await?;
|
||||||
|
self.emit_event(EventType::W30StatusUpdate {
|
||||||
|
msg_id: instance.id,
|
||||||
|
status_update_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns status updates as an JSON-array.
|
||||||
|
///
|
||||||
|
/// The updates may be filtered by a given status_update_id;
|
||||||
|
/// if no updates are available, an empty JSON-array is returned.
|
||||||
|
pub async fn get_w30_status_updates(
|
||||||
|
&self,
|
||||||
|
instance_msg_id: MsgId,
|
||||||
|
status_update_id: Option<StatusUpdateId>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let json_array = self
|
||||||
|
.sql
|
||||||
|
.query_map(
|
||||||
|
"SELECT payload FROM msgs_status_updates WHERE msg_id=? AND (1=? OR id=?)",
|
||||||
|
paramsv![
|
||||||
|
instance_msg_id,
|
||||||
|
if status_update_id.is_some() { 0 } else { 1 },
|
||||||
|
status_update_id.unwrap_or(StatusUpdateId(0))
|
||||||
|
],
|
||||||
|
|row| row.get::<_, String>(0),
|
||||||
|
|rows| {
|
||||||
|
let mut json_array = String::default();
|
||||||
|
for row in rows {
|
||||||
|
let json_entry = row?;
|
||||||
|
if !json_array.is_empty() {
|
||||||
|
json_array.push_str(",\n");
|
||||||
|
}
|
||||||
|
json_array.push_str(&json_entry);
|
||||||
|
}
|
||||||
|
Ok(json_array)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(format!("[{}]", json_array))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::{create_group_chat, send_msg, send_text_msg, ChatId, ProtectionStatus};
|
||||||
|
use crate::dc_receive_imf::dc_receive_imf;
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
use crate::Event;
|
||||||
|
use async_std::fs::File;
|
||||||
|
use async_std::io::WriteExt;
|
||||||
|
use async_std::prelude::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_is_w30_file() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert!(
|
||||||
|
!t.is_w30_file(
|
||||||
|
"issue_523.txt",
|
||||||
|
include_bytes!("../test-data/message/issue_523.txt")
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
t.is_w30_file(
|
||||||
|
"minimal.w30",
|
||||||
|
include_bytes!("../test-data/w30/minimal.w30")
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_w30_instance(t: &TestContext) -> Result<Message> {
|
||||||
|
let file = t.get_blobdir().join("index.w30");
|
||||||
|
File::create(&file)
|
||||||
|
.await?
|
||||||
|
.write_all("<html>ola!</html>".as_ref())
|
||||||
|
.await?;
|
||||||
|
let mut instance = Message::new(Viewtype::File);
|
||||||
|
instance.set_file(file.to_str().unwrap(), None);
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_w30_instance(t: &TestContext, chat_id: ChatId) -> Result<Message> {
|
||||||
|
let mut instance = create_w30_instance(t).await?;
|
||||||
|
let instance_msg_id = send_msg(t, chat_id, &mut instance).await?;
|
||||||
|
Message::load_from_db(t, instance_msg_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_send_w30_instance() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||||
|
|
||||||
|
// send as .w30 file
|
||||||
|
let instance = send_w30_instance(&t, chat_id).await?;
|
||||||
|
assert_eq!(instance.viewtype, Viewtype::W30);
|
||||||
|
assert_eq!(instance.get_filename(), Some("index.w30".to_string()));
|
||||||
|
assert_eq!(instance.chat_id, chat_id);
|
||||||
|
|
||||||
|
// sending using bad extension is not working, even when setting Viewtype to W30
|
||||||
|
let file = t.get_blobdir().join("index.html");
|
||||||
|
File::create(&file)
|
||||||
|
.await?
|
||||||
|
.write_all("<html>ola!</html>".as_ref())
|
||||||
|
.await?;
|
||||||
|
let mut instance = Message::new(Viewtype::W30);
|
||||||
|
instance.set_file(file.to_str().unwrap(), None);
|
||||||
|
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_receive_w30_instance() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
dc_receive_imf(
|
||||||
|
&t,
|
||||||
|
include_bytes!("../test-data/message/w30_good_extension.eml"),
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let instance = t.get_last_msg().await;
|
||||||
|
assert_eq!(instance.viewtype, Viewtype::W30);
|
||||||
|
assert_eq!(instance.get_filename(), Some("index.w30".to_string()));
|
||||||
|
|
||||||
|
dc_receive_imf(
|
||||||
|
&t,
|
||||||
|
include_bytes!("../test-data/message/w30_bad_extension.eml"),
|
||||||
|
"INBOX",
|
||||||
|
2,
|
||||||
|
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(), Some("index.html".to_string()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_delete_w30_instance() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||||
|
|
||||||
|
let mut instance = create_w30_instance(&t).await?;
|
||||||
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
||||||
|
let instance = chat_id.get_draft(&t).await?.unwrap();
|
||||||
|
t.send_w30_status_update(instance.id, "descr", "42").await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, None).await?,
|
||||||
|
"[42]".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_w30_status_updates(instance.id, None).await?,
|
||||||
|
"[]".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.sql
|
||||||
|
.count("SELECT COUNT(*) FROM msgs_status_updates;", paramsv![],)
|
||||||
|
.await?,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
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 instance = send_w30_instance(&t, chat_id).await?;
|
||||||
|
|
||||||
|
assert_eq!(t.get_w30_status_updates(instance.id, None).await?, "[]");
|
||||||
|
|
||||||
|
let id = t
|
||||||
|
.create_status_update_record(instance.id, "\n\n{\"foo\":\"bar\"}\n")
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, Some(id)).await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(t
|
||||||
|
.create_status_update_record(instance.id, "\n\n\n")
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(t
|
||||||
|
.create_status_update_record(instance.id, "bad json")
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, Some(id)).await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, None).await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
let id = t
|
||||||
|
.create_status_update_record(instance.id, r#"{"foo2":"bar2"}"#)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, Some(id)).await?,
|
||||||
|
r#"[{"foo2":"bar2"}]"#
|
||||||
|
);
|
||||||
|
t.create_status_update_record(instance.id, "true").await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, None).await?,
|
||||||
|
r#"[{"foo":"bar"},
|
||||||
|
{"foo2":"bar2"},
|
||||||
|
true]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
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_w30_instance(&t, chat_id).await?;
|
||||||
|
|
||||||
|
t.receive_status_update(instance.id, r#"[{"foo":"bar"}]"#)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, None).await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
t.receive_status_update(instance.id, r#" [ 42 , 23 ] "#)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_w30_status_updates(instance.id, None).await?,
|
||||||
|
r#"[{"foo":"bar"},
|
||||||
|
42,
|
||||||
|
23]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_send_w30_status_update() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
// Alice sends an w30 instance and a status update
|
||||||
|
let alice_chat = alice.create_chat(&bob).await;
|
||||||
|
let alice_instance = send_w30_instance(&alice, alice_chat.id).await?;
|
||||||
|
let sent1 = &alice.pop_sent_msg().await;
|
||||||
|
assert_eq!(alice_instance.viewtype, Viewtype::W30);
|
||||||
|
assert!(!sent1.payload().contains("report-type=status-update"));
|
||||||
|
|
||||||
|
let status_update_msg_id = alice
|
||||||
|
.send_w30_status_update(alice_instance.id, "descr text", r#"{"foo":"bar"}"#)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
let sent2 = &alice.pop_sent_msg().await;
|
||||||
|
let alice_update = Message::load_from_db(&alice, status_update_msg_id).await?;
|
||||||
|
assert!(alice_update.hidden);
|
||||||
|
assert_eq!(alice_update.viewtype, Viewtype::Text);
|
||||||
|
assert_eq!(alice_update.get_filename(), None);
|
||||||
|
assert_eq!(alice_update.text, Some("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_w30_status_updates(alice_instance.id, None)
|
||||||
|
.await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
alice
|
||||||
|
.send_w30_status_update(alice_instance.id, "bla text", r#"{"snipp":"snapp"}"#)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
alice
|
||||||
|
.get_w30_status_updates(alice_instance.id, None)
|
||||||
|
.await?,
|
||||||
|
r#"[{"foo":"bar"},
|
||||||
|
{"snipp":"snapp"}]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob receives all messages
|
||||||
|
bob.recv_msg(sent1).await;
|
||||||
|
let bob_instance = bob.get_last_msg().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::W30);
|
||||||
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
||||||
|
|
||||||
|
let (event_tx, event_rx) = async_std::channel::bounded(100);
|
||||||
|
bob.add_event_sink(move |event: Event| {
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
async move {
|
||||||
|
if let EventType::W30StatusUpdate { .. } = event.typ {
|
||||||
|
event_tx.try_send(event).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
bob.recv_msg(sent2).await;
|
||||||
|
let event = event_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.await
|
||||||
|
.expect("timeout waiting for W30StatusUpdate event")
|
||||||
|
.expect("missing W30StatusUpdate event");
|
||||||
|
match event.typ {
|
||||||
|
EventType::W30StatusUpdate {
|
||||||
|
msg_id,
|
||||||
|
status_update_id,
|
||||||
|
} => {
|
||||||
|
assert_eq!(
|
||||||
|
bob.get_w30_status_updates(msg_id, Some(status_update_id))
|
||||||
|
.await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
assert_eq!(msg_id, bob_instance.id);
|
||||||
|
}
|
||||||
|
_ => panic!("Wrong event type"),
|
||||||
|
}
|
||||||
|
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bob.get_w30_status_updates(bob_instance.id, None).await?,
|
||||||
|
r#"[{"foo":"bar"}]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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::W30);
|
||||||
|
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_draft_and_send_w30_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 w30 instance,
|
||||||
|
// status updates are not sent for drafts, therefore send_w30_status_update() returns Ok(None)
|
||||||
|
let mut alice_instance = create_w30_instance(&alice).await?;
|
||||||
|
alice_chat_id
|
||||||
|
.set_draft(&alice, Some(&mut alice_instance))
|
||||||
|
.await?;
|
||||||
|
let mut alice_instance = alice_chat_id.get_draft(&alice).await?.unwrap();
|
||||||
|
|
||||||
|
let status_update_msg_id = alice
|
||||||
|
.send_w30_status_update(alice_instance.id, "descr", r#"{"foo":"bar"}"#)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(status_update_msg_id, None);
|
||||||
|
let status_update_msg_id = alice
|
||||||
|
.send_w30_status_update(alice_instance.id, "descr", r#"42"#)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(status_update_msg_id, None);
|
||||||
|
|
||||||
|
// send w30 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::W30);
|
||||||
|
assert_eq!(alice_instance.get_filename(), Some("index.w30".to_string()));
|
||||||
|
assert_eq!(alice_instance.chat_id, alice_chat_id);
|
||||||
|
|
||||||
|
// bob receives the instance together with the initial updates in a single message
|
||||||
|
bob.recv_msg(&sent1).await;
|
||||||
|
let bob_instance = bob.get_last_msg().await;
|
||||||
|
assert_eq!(bob_instance.viewtype, Viewtype::W30);
|
||||||
|
assert_eq!(bob_instance.get_filename(), Some("index.w30".to_string()));
|
||||||
|
assert!(sent1.payload().contains("Content-Type: application/json"));
|
||||||
|
assert!(sent1.payload().contains("status-update.json"));
|
||||||
|
assert!(sent1.payload().contains(r#"{"foo":"bar"}"#));
|
||||||
|
assert_eq!(
|
||||||
|
bob.get_w30_status_updates(bob_instance.id, None).await?,
|
||||||
|
r#"[{"foo":"bar"},
|
||||||
|
42]"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_send_w30_status_update_to_non_w30() -> 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_w30_status_update(msg_id, "descr", r#"{"foo":"bar"}"#)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
22
test-data/message/w30_bad_extension.eml
Normal file
22
test-data/message/w30_bad_extension.eml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Subject: W30 object attached
|
||||||
|
Message-ID: 67890@example.org
|
||||||
|
Date: Fri, 03 Dec 2021 10:00:27 +0000
|
||||||
|
To: alice@example.org
|
||||||
|
From: bob@example.org
|
||||||
|
Chat-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed; boundary="==BREAK=="
|
||||||
|
|
||||||
|
|
||||||
|
--==BREAK==
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
w30 with correct extension;
|
||||||
|
this is not that important here.
|
||||||
|
|
||||||
|
--==BREAK==
|
||||||
|
Content-Type: application/html+w30
|
||||||
|
Content-Disposition: attachment; filename=index.html
|
||||||
|
|
||||||
|
<html>hey!<html>
|
||||||
|
|
||||||
|
--==BREAK==--
|
||||||
22
test-data/message/w30_good_extension.eml
Normal file
22
test-data/message/w30_good_extension.eml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Subject: W30 object attached
|
||||||
|
Message-ID: 12345@example.org
|
||||||
|
Date: Fri, 03 Dec 2021 10:00:27 +0000
|
||||||
|
To: alice@example.org
|
||||||
|
From: bob@example.org
|
||||||
|
Chat-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed; boundary="==BREAK=="
|
||||||
|
|
||||||
|
|
||||||
|
--==BREAK==
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
w30 with good extension;
|
||||||
|
the mimetype is ignored then.
|
||||||
|
|
||||||
|
--==BREAK==
|
||||||
|
Content-Type: text/html
|
||||||
|
Content-Disposition: attachment; filename=index.w30
|
||||||
|
|
||||||
|
<html>hey!<html>
|
||||||
|
|
||||||
|
--==BREAK==--
|
||||||
BIN
test-data/w30/minimal.w30
Normal file
BIN
test-data/w30/minimal.w30
Normal file
Binary file not shown.
Reference in New Issue
Block a user