mirror of
https://github.com/chatmail/core.git
synced 2026-04-22 16:06:30 +03:00
Start implementing #1994 TODO (in later PRs): - Add a hint to the watch settings that all folders are fetched from time to time (to be done in the individual UIs) - folder names are case-insensitive, so double-check that all comparisons are case-insensitive - The `scan_folders.rs` file didn't get as large as I expected and it's probably not worth it having an extra file for it. But if there are no objections, I'll make another PR to rename it to `folders.rs` and also put into it `configure_folders()` from `imap/mod.rs` and `needs_move()` with all its tests from `message.rs`. Done: - Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them, what do we do about this? The most reliable way to detect such messages that we found up to now is: If there is no `Received` header AND it's not in the `ConfiguredSentbox`, then ignore the email. - before or after INBOX idle trigger a new "scan all folders for messages". It does a "list folders" and then goes through all folders with select-statements, checking if "next-uid" was changed since checked last time. This might be batchable but in any case should not consume a lot of traffic. We might debounce this scan activity to happen at most every N minutes - if next-uid changed for a folder, we "prefetch" and "fetch" DC-messages as as needed ("dc-messages" are not just those with "Chat-Version" headers, but can also be regular emails) - if we discover DC-messages in folders that have the "/Spam" flag (maybe excluding ContactRequests) we automatically move them to INBOX/DeltaChat folder to help provider-spam-systems to regard this contact/mail as non-spam - for now, we do not change any user visible option, but introduce this "scan all" automatically and on top of what exists. The DeltaChat folder-watching does not perform scan-all-folders (maybe with the exception to trigger scan-all also with DeltaChat if INBOX is not watched) - Tests (except if you have ideas to improve them) - all folders, their last uidvalidity, next-seen etc. are kept in a separate "imap-sync" sqlite table. Maybe this can be used to streamline some of the "Sent" folder and "DeltaChat" folder detection code we already have. - We now also move self-sent messages from the Inbox to the Sent folder if `mvbox_move` is off, as this was very easy to do now. This way, we now behave more like a normal MUA if the user wants this. FOR LATER PRs: - maybe for the first 50 messages or so, we could reduce the IDLE-timeout (currently 23 minutes or so) to faster detect messages sent to non-inbox folders. However, on Android and iOS, we would likely trigger scan-all when the app moves to foreground, and so it might not be neccessary to reduce the current idle-timeout at least for them. We can leave this "faster discovery" question for the end, after we move to real-life testing. - (Later on, after the above works, we can consider heuristics on which folders to perform IDLE on, and remove the Watch-folder options (inbox, deltachat, sent). We tried to find a safe scheme for already doing it but failed to fine one, too many unknowns, also some questions regarding multi-device (you might have different settings with each of it, one moves, the other doesn't etc.) so we postponed this in favor of the above incremental suggestion.) * Start implementing #1994 * Add debug logs, it seems like the SQL migration can go into another pr * Let fetch_new_messages return whether there are new emails * Code style * Don't prefetch if there are no new emails * clippy * Even more debug logs * If the folder was not newly selected, return always try to fetch as uid_next is probably outdated * Fix new bug * Recognize spam folder * if we discover DC-messages in folders that have the "/Spam" flag (excluding ContactRequests) we automatically move them to INBOX/DeltaChat folder to help provider-spam-systems to regard this contact/mail as non-spam * Clippy, prioritize folder_meaning over folder_name_meaning * Add a first test, for the first day after installation only debounce to 2s * Start adding two tests (both of them fail) * Don't abort folder scan if one folder fails * More consts * Replace bool return value by enum * Split test up into multiple tests * Print logs during rust tests * Rust tests pass now * . * One of the Python tests passes now - reconfigure folders during scanning * Make the last test pass - Delete emails in all folders when starting the test, not only inbox and mvbox The problem had been that emails were left in the folder "xyz" * lint * DB migration (untested) * Store uid_next in SQL instead of lastseen in a config * Revert "If Inbox-watch is disabled and enabled again, do not fetch emails from in between" all folders are always watched, anyway * clippy, rm debug logs, comments * Codestyle, comments * fixing things again * Fix another test: don't fetch from uid_next-1 but uid_next; make some {} to {:#} so that we can use `.context(...)` * move self-sent, non-setupmessage chat messages to the Sent folder if `mvbox_move` is off * comment * Comments, make sure things work even if there is no uid_next * Style * Comments * The rust test tested wrongly * comments, small codestyle change * Ignore emails that are probably only drafts Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them. So: If there is no Received header AND it's not in the ConfiguredSentbox, then ignore the email. Also: Add test. * Fix occasional test failure, it was introduced as DC now moves messages from Inbox to Sent * Add `Received` header to the rust tests * After this PR we will always watch all folders and delete messages there if server_delete is enabled. So, for people who have server_delete on, disable it and add a hint to the devicechat * comment, small fix * link2xt's first review * Use ON CONFLICT(FOLDER) DO to update and if it doesn't exist, then insert Reason from link2xt: We had a problem with multiple peerstates inserted due to key fingerprint parsing error previously. With logic in Rust a similar problem can occur: an UPDATE can fail for reasons other than a conflict. PRIMARY KEY should ensure uniqueness in this case, but anyway. * Remove two TODO statements, remove fetch_new_messages: ignoring uid {}, uid_next was {} log * Next TODO: Make uidvalidity and uid_next DEFAULT 0 * rm two TODOs, Seems like we are not going to `exclude folders that are watched anyway` in this PR * small tweak: Handle instants more carefully * Add scan_all_folders_debounce_secs config for tests, set debounce to 60s (before it was just 2s during the first day) * Don't use bold letters for the device message * React to changes in the folders better Before, if there was a configured Sent folder, but then it got removed and replaced with another folder with a name meaning "Sent" but without Sent flag, it would be ignored. So, instead of checking against ConfiguredSentboxFolder, create two Option variables at the beginning of the loop and replace them with Some if it is None. At the end of the loop, store the new values into ConfiguredSendboxFolder and ConfiguredSpamFolder, even if it is None. Also, derive some useful traits. * move job: Return a meaningful error if server_folder is None instead of panicing * small error-handling fix * Fix test_fetch_existing() python test Before, we sometimes got a race condition where scan_folders() sees that there is a Sentbox and saves this info after we set the ConfiguredSentbox to None and before the message is sent. So, just expect that the message is moved to the sentbox. * migration is 72 now * rm 2 TODOs, Don't infinitely retry when dc_receive_imf() returns Err * clippy: Remove glob imports * Delete created folders at the beginning of tests (some created folders made problems in the next tests because) * Improve resetting accounts between tests
644 lines
22 KiB
Rust
644 lines
22 KiB
Rust
//! # Ephemeral messages
|
|
//!
|
|
//! Ephemeral messages are messages that have an Ephemeral-Timer
|
|
//! header attached to them, which specifies time in seconds after
|
|
//! which the message should be deleted both from the device and from
|
|
//! the server. The timer is started when the message is marked as
|
|
//! seen, which usually happens when its contents is displayed on
|
|
//! device screen.
|
|
//!
|
|
//! Each chat, including 1:1, group chats and "saved messages" chat,
|
|
//! has its own ephemeral timer setting, which is applied to all
|
|
//! messages sent to the chat. The setting is synchronized to all the
|
|
//! devices participating in the chat by applying the timer value from
|
|
//! all received messages, including BCC-self ones, to the chat. This
|
|
//! way the setting is eventually synchronized among all participants.
|
|
//!
|
|
//! When user changes ephemeral timer setting for the chat, a system
|
|
//! message is automatically sent to update the setting for all
|
|
//! participants. This allows changing the setting for a chat like any
|
|
//! group chat setting, e.g. name and avatar, without the need to
|
|
//! write an actual message.
|
|
//!
|
|
//! ## Device settings
|
|
//!
|
|
//! In addition to per-chat ephemeral message setting, each device has
|
|
//! two global user-configured settings that complement per-chat
|
|
//! settings: `delete_device_after` and `delete_server_after`. These
|
|
//! settings are not synchronized among devices and apply to all
|
|
//! messages known to the device, including messages sent or received
|
|
//! before configuring the setting.
|
|
//!
|
|
//! `delete_device_after` configures the maximum time device is
|
|
//! storing the messages locally. `delete_server_after` configures the
|
|
//! time after which device will delete the messages it knows about
|
|
//! from the server.
|
|
//!
|
|
//! ## How messages are deleted
|
|
//!
|
|
//! When the message is deleted locally, its contents is removed and
|
|
//! it is moved to the trash chat. This database entry is then used to
|
|
//! track the Message-ID and corresponding IMAP folder and UID until
|
|
//! the message is deleted from the server. Vice versa, when device
|
|
//! deletes the message from the server, it removes IMAP folder and
|
|
//! UID information, but keeps the message contents. When database
|
|
//! entry is both moved to trash chat and does not contain UID
|
|
//! information, it is deleted from the database, leaving no trace of
|
|
//! the message.
|
|
//!
|
|
//! ## When messages are deleted
|
|
//!
|
|
//! Local deletion happens when the chatlist or chat is loaded. A
|
|
//! `MsgsChanged` event is emitted when a message deletion is due, to
|
|
//! make UI reload displayed messages and cause actual deletion.
|
|
//!
|
|
//! Server deletion happens by generating IMAP deletion jobs based on
|
|
//! the database entries which are expired either according to their
|
|
//! ephemeral message timers or global `delete_server_after` setting.
|
|
|
|
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
|
|
use crate::constants::{
|
|
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
|
};
|
|
use crate::context::Context;
|
|
use crate::dc_tools::time;
|
|
use crate::error::{ensure, Error};
|
|
use crate::events::EventType;
|
|
use crate::message::{Message, MessageState, MsgId};
|
|
use crate::mimeparser::SystemMessage;
|
|
use crate::sql;
|
|
use crate::stock::StockMessage;
|
|
use async_std::task;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::convert::{TryFrom, TryInto};
|
|
use std::num::ParseIntError;
|
|
use std::str::FromStr;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
|
pub enum Timer {
|
|
Disabled,
|
|
Enabled { duration: u32 },
|
|
}
|
|
|
|
impl Timer {
|
|
pub fn to_u32(self) -> u32 {
|
|
match self {
|
|
Self::Disabled => 0,
|
|
Self::Enabled { duration } => duration,
|
|
}
|
|
}
|
|
|
|
pub fn from_u32(duration: u32) -> Self {
|
|
if duration == 0 {
|
|
Self::Disabled
|
|
} else {
|
|
Self::Enabled { duration }
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Timer {
|
|
fn default() -> Self {
|
|
Self::Disabled
|
|
}
|
|
}
|
|
|
|
impl ToString for Timer {
|
|
fn to_string(&self) -> String {
|
|
self.to_u32().to_string()
|
|
}
|
|
}
|
|
|
|
impl FromStr for Timer {
|
|
type Err = ParseIntError;
|
|
|
|
fn from_str(input: &str) -> Result<Timer, ParseIntError> {
|
|
input.parse::<u32>().map(Self::from_u32)
|
|
}
|
|
}
|
|
|
|
impl rusqlite::types::ToSql for Timer {
|
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
|
let val = rusqlite::types::Value::Integer(match self {
|
|
Self::Disabled => 0,
|
|
Self::Enabled { duration } => i64::from(*duration),
|
|
});
|
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
impl rusqlite::types::FromSql for Timer {
|
|
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
i64::column_result(value).and_then(|value| {
|
|
if value == 0 {
|
|
Ok(Self::Disabled)
|
|
} else if let Ok(duration) = u32::try_from(value) {
|
|
Ok(Self::Enabled { duration })
|
|
} else {
|
|
Err(rusqlite::types::FromSqlError::OutOfRange(value))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ChatId {
|
|
/// Get ephemeral message timer value in seconds.
|
|
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
|
|
let timer = context
|
|
.sql
|
|
.query_get_value_result(
|
|
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
|
paramsv![self],
|
|
)
|
|
.await?;
|
|
Ok(timer.unwrap_or_default())
|
|
}
|
|
|
|
/// Set ephemeral timer value without sending a message.
|
|
///
|
|
/// Used when a message arrives indicating that someone else has
|
|
/// changed the timer value for a chat.
|
|
pub(crate) async fn inner_set_ephemeral_timer(
|
|
self,
|
|
context: &Context,
|
|
timer: Timer,
|
|
) -> Result<(), Error> {
|
|
ensure!(!self.is_special(), "Invalid chat ID");
|
|
|
|
context
|
|
.sql
|
|
.execute(
|
|
"UPDATE chats
|
|
SET ephemeral_timer=?
|
|
WHERE id=?;",
|
|
paramsv![timer, self],
|
|
)
|
|
.await?;
|
|
|
|
context.emit_event(EventType::ChatEphemeralTimerModified {
|
|
chat_id: self,
|
|
timer,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// Set ephemeral message timer value in seconds.
|
|
///
|
|
/// If timer value is 0, disable ephemeral message timer.
|
|
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
|
|
if timer == self.get_ephemeral_timer(context).await? {
|
|
return Ok(());
|
|
}
|
|
self.inner_set_ephemeral_timer(context, timer).await?;
|
|
let mut msg = Message::new(Viewtype::Text);
|
|
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
|
|
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
|
|
if let Err(err) = send_msg(context, self, &mut msg).await {
|
|
error!(
|
|
context,
|
|
"Failed to send a message about ephemeral message timer change: {:?}", err
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
|
|
pub(crate) async fn stock_ephemeral_timer_changed(
|
|
context: &Context,
|
|
timer: Timer,
|
|
from_id: u32,
|
|
) -> String {
|
|
let stock_message = match timer {
|
|
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
|
|
Timer::Enabled { duration } => match duration {
|
|
60 => StockMessage::MsgEphemeralTimerMinute,
|
|
3600 => StockMessage::MsgEphemeralTimerHour,
|
|
86400 => StockMessage::MsgEphemeralTimerDay,
|
|
604_800 => StockMessage::MsgEphemeralTimerWeek,
|
|
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
|
|
_ => StockMessage::MsgEphemeralTimerEnabled,
|
|
},
|
|
};
|
|
|
|
context
|
|
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
|
|
.await
|
|
}
|
|
|
|
impl MsgId {
|
|
/// Returns ephemeral message timer value for the message.
|
|
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
|
|
let res = match context
|
|
.sql
|
|
.query_get_value_result(
|
|
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
|
paramsv![self],
|
|
)
|
|
.await?
|
|
{
|
|
None | Some(0) => Timer::Disabled,
|
|
Some(duration) => Timer::Enabled { duration },
|
|
};
|
|
Ok(res)
|
|
}
|
|
|
|
/// Starts ephemeral message timer for the message if it is not started yet.
|
|
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
|
|
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
|
let ephemeral_timestamp = time() + i64::from(duration);
|
|
|
|
context
|
|
.sql
|
|
.execute(
|
|
"UPDATE msgs SET ephemeral_timestamp = ? \
|
|
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
|
AND id = ?",
|
|
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
|
)
|
|
.await?;
|
|
schedule_ephemeral_task(context).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Deletes messages which are expired according to
|
|
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
|
///
|
|
/// Returns true if any message is deleted, so caller can emit
|
|
/// MsgsChanged event. If nothing has been deleted, returns
|
|
/// false. This function does not emit the MsgsChanged event itself,
|
|
/// because it is also called when chatlist is reloaded, and emitting
|
|
/// MsgsChanged there will cause infinite reload loop.
|
|
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
|
|
let mut updated = context
|
|
.sql
|
|
.execute(
|
|
"UPDATE msgs \
|
|
SET chat_id=?, txt='', txt_raw='', from_id=0, to_id=0, param='' \
|
|
WHERE \
|
|
ephemeral_timestamp != 0 \
|
|
AND ephemeral_timestamp <= ? \
|
|
AND chat_id != ?",
|
|
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
|
)
|
|
.await?
|
|
> 0;
|
|
|
|
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
|
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
|
.await
|
|
.unwrap_or_default()
|
|
.0;
|
|
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
|
.await
|
|
.unwrap_or_default()
|
|
.0;
|
|
|
|
let threshold_timestamp = time() - delete_device_after;
|
|
|
|
// Delete expired messages
|
|
//
|
|
// Only update the rows that have to be updated, to avoid emitting
|
|
// unnecessary "chat modified" events.
|
|
let rows_modified = context
|
|
.sql
|
|
.execute(
|
|
"UPDATE msgs \
|
|
SET txt = 'DELETED', chat_id = ? \
|
|
WHERE timestamp < ? \
|
|
AND chat_id > ? \
|
|
AND chat_id != ? \
|
|
AND chat_id != ?",
|
|
paramsv![
|
|
DC_CHAT_ID_TRASH,
|
|
threshold_timestamp,
|
|
DC_CHAT_ID_LAST_SPECIAL,
|
|
self_chat_id,
|
|
device_chat_id
|
|
],
|
|
)
|
|
.await?;
|
|
|
|
updated |= rows_modified > 0;
|
|
}
|
|
|
|
schedule_ephemeral_task(context).await;
|
|
Ok(updated)
|
|
}
|
|
|
|
/// Schedule a task to emit MsgsChanged event when the next local
|
|
/// deletion happens. Existing task is cancelled to make sure at most
|
|
/// one such task is scheduled at a time.
|
|
///
|
|
/// UI is expected to reload the chatlist or the chat in response to
|
|
/// MsgsChanged event, this will trigger actual deletion.
|
|
///
|
|
/// This takes into account only per-chat timeouts, because global device
|
|
/// timeouts are at least one hour long and deletion is triggered often enough
|
|
/// by user actions.
|
|
pub async fn schedule_ephemeral_task(context: &Context) {
|
|
let ephemeral_timestamp: Option<i64> = match context
|
|
.sql
|
|
.query_get_value_result(
|
|
"SELECT ephemeral_timestamp \
|
|
FROM msgs \
|
|
WHERE ephemeral_timestamp != 0 \
|
|
AND chat_id != ? \
|
|
ORDER BY ephemeral_timestamp ASC \
|
|
LIMIT 1",
|
|
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
|
)
|
|
.await
|
|
{
|
|
Err(err) => {
|
|
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
|
|
return;
|
|
}
|
|
Ok(ephemeral_timestamp) => ephemeral_timestamp,
|
|
};
|
|
|
|
// Cancel existing task, if any
|
|
if let Some(ephemeral_task) = context.ephemeral_task.write().await.take() {
|
|
ephemeral_task.cancel().await;
|
|
}
|
|
|
|
if let Some(ephemeral_timestamp) = ephemeral_timestamp {
|
|
let now = SystemTime::now();
|
|
let until = UNIX_EPOCH
|
|
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
|
|
+ Duration::from_secs(1);
|
|
|
|
if let Ok(duration) = until.duration_since(now) {
|
|
// Schedule a task, ephemeral_timestamp is in the future
|
|
let context1 = context.clone();
|
|
let ephemeral_task = task::spawn(async move {
|
|
async_std::task::sleep(duration).await;
|
|
emit_event!(
|
|
context1,
|
|
EventType::MsgsChanged {
|
|
chat_id: ChatId::new(0),
|
|
msg_id: MsgId::new(0)
|
|
}
|
|
);
|
|
});
|
|
*context.ephemeral_task.write().await = Some(ephemeral_task);
|
|
} else {
|
|
// Emit event immediately
|
|
emit_event!(
|
|
context,
|
|
EventType::MsgsChanged {
|
|
chat_id: ChatId::new(0),
|
|
msg_id: MsgId::new(0)
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns ID of any expired message that should be deleted from the server.
|
|
///
|
|
/// It looks up the trash chat too, to find messages that are already
|
|
/// deleted locally, but not deleted on the server.
|
|
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
|
let now = time();
|
|
|
|
let threshold_timestamp = match context.get_config_delete_server_after().await {
|
|
None => 0,
|
|
Some(delete_server_after) => now - delete_server_after,
|
|
};
|
|
|
|
context
|
|
.sql
|
|
.query_row_optional(
|
|
"SELECT id FROM msgs \
|
|
WHERE ( \
|
|
timestamp < ? \
|
|
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
|
) \
|
|
AND server_uid != 0 \
|
|
LIMIT 1",
|
|
paramsv![threshold_timestamp, now],
|
|
|row| row.get::<_, MsgId>(0),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Start ephemeral timers for seen messages if they are not started
|
|
/// yet.
|
|
///
|
|
/// It is possible that timers are not started due to a missing or
|
|
/// failed `MsgId.start_ephemeral_timer()` call, either in the current
|
|
/// or previous version of Delta Chat.
|
|
///
|
|
/// This function is supposed to be called in the background,
|
|
/// e.g. from housekeeping task.
|
|
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
|
|
context
|
|
.sql
|
|
.execute(
|
|
"UPDATE msgs \
|
|
SET ephemeral_timestamp = ? + ephemeral_timer \
|
|
WHERE ephemeral_timer > 0 \
|
|
AND ephemeral_timestamp = 0 \
|
|
AND state NOT IN (?, ?, ?)",
|
|
paramsv![
|
|
time(),
|
|
MessageState::InFresh,
|
|
MessageState::InNoticed,
|
|
MessageState::OutDraft
|
|
],
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use async_std::task::sleep;
|
|
|
|
use super::*;
|
|
use crate::test_utils::TestContext;
|
|
use crate::{
|
|
chat::{self, Chat, ChatItem},
|
|
dc_tools::IsNoneOrEmpty,
|
|
};
|
|
|
|
#[async_std::test]
|
|
async fn test_stock_ephemeral_messages() {
|
|
let context = TestContext::new().await.ctx;
|
|
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
|
|
"Message deletion timer is disabled by me."
|
|
);
|
|
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await,
|
|
"Message deletion timer is disabled."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
|
|
"Message deletion timer is set to 1 s."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
|
|
"Message deletion timer is set to 30 s."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
|
|
"Message deletion timer is set to 1 minute."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
|
|
"Message deletion timer is set to 1 hour."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(
|
|
&context,
|
|
Timer::Enabled {
|
|
duration: 24 * 60 * 60
|
|
},
|
|
0
|
|
)
|
|
.await,
|
|
"Message deletion timer is set to 1 day."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(
|
|
&context,
|
|
Timer::Enabled {
|
|
duration: 7 * 24 * 60 * 60
|
|
},
|
|
0
|
|
)
|
|
.await,
|
|
"Message deletion timer is set to 1 week."
|
|
);
|
|
assert_eq!(
|
|
stock_ephemeral_timer_changed(
|
|
&context,
|
|
Timer::Enabled {
|
|
duration: 4 * 7 * 24 * 60 * 60
|
|
},
|
|
0
|
|
)
|
|
.await,
|
|
"Message deletion timer is set to 4 weeks."
|
|
);
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_ephemeral_timer() -> crate::error::Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
|
|
let chat_alice = alice.create_chat(&bob).await.id;
|
|
let chat_bob = bob.create_chat(&alice).await.id;
|
|
|
|
// Alice sends message to Bob
|
|
let mut msg = Message::new(Viewtype::Text);
|
|
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
let sent = alice.pop_sent_msg().await;
|
|
bob.recv_msg(&sent).await;
|
|
|
|
// Alice sends second message to Bob, with no timer
|
|
let mut msg = Message::new(Viewtype::Text);
|
|
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
let sent = alice.pop_sent_msg().await;
|
|
|
|
assert_eq!(
|
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
Timer::Disabled
|
|
);
|
|
|
|
// Bob sets ephemeral timer and sends a message about timer change
|
|
chat_bob
|
|
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
|
.await?;
|
|
let sent_timer_change = bob.pop_sent_msg().await;
|
|
|
|
assert_eq!(
|
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
Timer::Enabled { duration: 60 }
|
|
);
|
|
|
|
// Bob receives message from Alice.
|
|
// Alice message has no timer. However, Bob should not disable timer,
|
|
// because Alice replies to old message.
|
|
bob.recv_msg(&sent).await;
|
|
|
|
assert_eq!(
|
|
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
|
Timer::Disabled
|
|
);
|
|
assert_eq!(
|
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
Timer::Enabled { duration: 60 }
|
|
);
|
|
|
|
// Alice receives message from Bob
|
|
alice.recv_msg(&sent_timer_change).await;
|
|
|
|
assert_eq!(
|
|
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
|
Timer::Enabled { duration: 60 }
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_ephemeral_delete_msgs() {
|
|
let t = TestContext::new_alice().await;
|
|
let chat = t.get_self_chat().await;
|
|
|
|
t.send_text(chat.id, "Saved message, which we delete manually")
|
|
.await;
|
|
let msg = t.get_last_msg_in(chat.id).await;
|
|
msg.id.delete_from_db(&t).await.unwrap();
|
|
check_msg_was_deleted(&t, &chat, msg.id).await;
|
|
|
|
chat.id
|
|
.set_ephemeral_timer(&t, Timer::Enabled { duration: 1 })
|
|
.await
|
|
.unwrap();
|
|
let msg = t
|
|
.send_text(chat.id, "Saved message, disappearing after 1s")
|
|
.await;
|
|
|
|
sleep(Duration::from_millis(1100)).await;
|
|
|
|
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
|
|
}
|
|
|
|
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
|
let chat_items = chat::get_chat_msgs(&t, chat.id, 0, None).await;
|
|
// Check that the chat is empty except for possibly info messages:
|
|
for item in &chat_items {
|
|
if let ChatItem::Message { msg_id } = item {
|
|
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
|
assert!(msg.is_info())
|
|
}
|
|
}
|
|
|
|
// Check that if there is a message left, the text and metadata are gone
|
|
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
|
|
assert_eq!(msg.from_id, 0);
|
|
assert_eq!(msg.to_id, 0);
|
|
assert!(msg.text.is_none_or_empty(), msg.text);
|
|
let rawtxt: Option<String> = t
|
|
.sql
|
|
.query_get_value(&t, "SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
|
.await;
|
|
assert!(rawtxt.is_none_or_empty(), rawtxt);
|
|
}
|
|
}
|
|
}
|