mirror of
https://github.com/chatmail/core.git
synced 2026-05-03 21:36:29 +03:00
Merge remote-tracking branch 'origin/master' into feat/async-jobs
This commit is contained in:
225
src/chat.rs
225
src/chat.rs
@@ -15,7 +15,7 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Error;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::events::Event;
|
||||
use crate::job::{self, Action};
|
||||
use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId};
|
||||
@@ -340,13 +340,12 @@ impl ChatId {
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -399,6 +398,62 @@ impl ChatId {
|
||||
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
|
||||
}
|
||||
|
||||
async fn parent_query<T, F>(
|
||||
self,
|
||||
context: &Context,
|
||||
fields: &str,
|
||||
f: F,
|
||||
) -> sql::Result<Option<T>>
|
||||
where
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
{
|
||||
let sql = &context.sql;
|
||||
let query = format!(
|
||||
"SELECT {} \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;",
|
||||
fields
|
||||
);
|
||||
sql.query_row_optional(
|
||||
query,
|
||||
paramsv![
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed
|
||||
],
|
||||
f,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_parent_mime_headers(self, context: &Context) -> Option<(String, String, String)> {
|
||||
let collect = |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?, row.get(2)?));
|
||||
self.parent_query(
|
||||
context,
|
||||
"rfc724_mid, mime_in_reply_to, mime_references",
|
||||
collect,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
async fn parent_is_encrypted(self, context: &Context) -> Result<bool, Error> {
|
||||
let collect = |row: &rusqlite::Row| Ok(row.get(0)?);
|
||||
let packed: Option<String> = self.parent_query(context, "param", collect).await?;
|
||||
|
||||
if let Some(ref packed) = packed {
|
||||
let param = packed.parse::<Params>()?;
|
||||
Ok(param.exists(Param::GuaranteeE2ee))
|
||||
} else {
|
||||
// No messages
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -580,44 +635,6 @@ impl Chat {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub async fn get_subtitle(&self, context: &Context) -> String {
|
||||
// returns either the address or the number of chat members
|
||||
|
||||
if self.typ == Chattype::Single && self.param.exists(Param::Selftalk) {
|
||||
return context
|
||||
.stock_str(StockMessage::SelfTalkSubTitle)
|
||||
.await
|
||||
.into();
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
return context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT c.addr
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?;",
|
||||
paramsv![self.id],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| "Err".into());
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup {
|
||||
if self.id.is_deaddrop() {
|
||||
return context.stock_str(StockMessage::DeadDrop).await.into();
|
||||
}
|
||||
let cnt = get_chat_contact_cnt(context, self.id).await;
|
||||
return context
|
||||
.stock_string_repl_int(StockMessage::Member, cnt as i32)
|
||||
.await;
|
||||
}
|
||||
|
||||
"Err".to_string()
|
||||
}
|
||||
|
||||
fn parent_query(fields: &str) -> String {
|
||||
// Check for server_uid guarantees that we don't
|
||||
// select a draft or undelivered message.
|
||||
@@ -717,7 +734,6 @@ impl Chat {
|
||||
.await
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(std::path::PathBuf::new),
|
||||
subtitle: self.get_subtitle(context).await,
|
||||
draft,
|
||||
is_muted: self.is_muted(),
|
||||
})
|
||||
@@ -873,7 +889,7 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
|
||||
if can_encrypt && (all_mutual || self.parent_is_encrypted(context).await?) {
|
||||
if can_encrypt && (all_mutual || self.id.parent_is_encrypted(context).await?) {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
}
|
||||
@@ -888,7 +904,7 @@ impl Chat {
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
if !self.is_self_talk() {
|
||||
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
self.get_parent_mime_headers(context).await
|
||||
self.id.get_parent_mime_headers(context).await
|
||||
{
|
||||
if !parent_rfc724_mid.is_empty() {
|
||||
new_in_reply_to = parent_rfc724_mid.clone();
|
||||
@@ -1067,9 +1083,6 @@ pub struct ChatInfo {
|
||||
/// currently.
|
||||
pub profile_image: std::path::PathBuf,
|
||||
|
||||
/// Subtitle for the chat.
|
||||
pub subtitle: String,
|
||||
|
||||
/// The draft message text.
|
||||
///
|
||||
/// If the chat has not draft this is an empty string.
|
||||
@@ -1535,7 +1548,7 @@ pub async fn get_chat_msgs(
|
||||
flags: u32,
|
||||
marker1before: Option<MsgId>,
|
||||
) -> Vec<MsgId> {
|
||||
match hide_device_expired_messages(context).await {
|
||||
match delete_device_expired_messages(context).await {
|
||||
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
|
||||
Ok(messages_deleted) => {
|
||||
if messages_deleted {
|
||||
@@ -1700,11 +1713,11 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hides messages which are expired according to "delete_device_after" setting.
|
||||
/// Deletes messages which are expired according to "delete_device_after" setting.
|
||||
///
|
||||
/// Returns true if any message is hidden, so event can be emitted. If nothing
|
||||
/// has been hidden, returns false.
|
||||
pub async fn hide_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
/// Returns true if any message is deleted, so event can be emitted. If nothing
|
||||
/// has been deleted, returns false.
|
||||
pub async fn delete_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
@@ -1717,7 +1730,7 @@ pub async fn hide_device_expired_messages(context: &Context) -> Result<bool, Err
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
|
||||
// Hide expired messages
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
@@ -1725,13 +1738,14 @@ pub async fn hide_device_expired_messages(context: &Context) -> Result<bool, Err
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', hidden = 1 \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ? \
|
||||
AND NOT hidden",
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
@@ -1822,17 +1836,17 @@ pub async fn get_next_media(
|
||||
msg_type3,
|
||||
)
|
||||
.await;
|
||||
for i in 0..list.len() {
|
||||
if curr_msg_id == list[i] {
|
||||
for (i, msg_id) in list.iter().enumerate() {
|
||||
if curr_msg_id == *msg_id {
|
||||
match direction {
|
||||
Direction::Forward => {
|
||||
if i + 1 < list.len() {
|
||||
ret = Some(list[i + 1]);
|
||||
ret = list.get(i + 1).copied();
|
||||
}
|
||||
}
|
||||
Direction::Backward => {
|
||||
if i >= 1 {
|
||||
ret = Some(list[i - 1]);
|
||||
ret = list.get(i - 1).copied();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1918,38 +1932,56 @@ pub async fn create_group_chat(
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// add a contact to the chats_contact table
|
||||
pub(crate) async fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> bool {
|
||||
// add a contact to a chat; the function does not check the type or if any of the record exist or are already
|
||||
// added to the chat!
|
||||
context
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
paramsv![chat_id, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"could not add {} to chat {} table: {}", contact_id, chat_id, err
|
||||
);
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// remove a contact from the chats_contact table
|
||||
pub(crate) async fn remove_from_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> bool {
|
||||
// remove a contact from the chats_contact table unconditionally
|
||||
// the function does not check the type or if the record exist
|
||||
context
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
paramsv![chat_id, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
context,
|
||||
"could not remove contact {:?} from chat {:?}", contact_id, chat_id
|
||||
);
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a contact to the chat.
|
||||
@@ -2049,15 +2081,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
Ok(true)
|
||||
}
|
||||
@@ -2211,19 +2235,18 @@ pub async fn set_muted(
|
||||
duration: MuteDuration,
|
||||
) -> Result<(), Error> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
if real_group_exists(context, chat_id).await
|
||||
&& context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![duration, chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![duration, chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("Failed to set name");
|
||||
bail!("Failed to set mute duration, chat might not exist -");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2257,8 +2280,7 @@ pub async fn remove_contact_from_chat(
|
||||
"Cannot remove contact from chat; self not in group.".into()
|
||||
)
|
||||
);
|
||||
} else if remove_from_chat_contacts_table(context, chat_id, contact_id).await {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
} else {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
@@ -2289,14 +2311,20 @@ pub async fn remove_contact_from_chat(
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
// we remove the member from the chat after constructing the
|
||||
// to-be-send message. If between send_msg() and here the
|
||||
// process dies the user will have to re-do the action. It's
|
||||
// better than the other way round: you removed
|
||||
// someone from DB but no peer or device gets to know about it and
|
||||
// group membership is thus different on different devices.
|
||||
// Note also that sending a message needs all recipients
|
||||
// in order to correctly determine encryption so if we
|
||||
// removed it first, it would complicate the
|
||||
// check/encryption logic.
|
||||
success = remove_from_chat_contacts_table(context, chat_id, contact_id).await;
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2781,7 +2809,6 @@ mod tests {
|
||||
"is_sending_locations": false,
|
||||
"color": 15895624,
|
||||
"profile_image": "",
|
||||
"subtitle": "bob@example.com",
|
||||
"draft": "",
|
||||
"is_muted": false
|
||||
}
|
||||
@@ -3402,4 +3429,18 @@ mod tests {
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parent_is_encrypted() {
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user