diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 516dce48f..d572e466b 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -22,11 +22,13 @@ use std::sync::RwLock; use libc::uintptr_t; use num_traits::{FromPrimitive, ToPrimitive}; +use deltachat::constants::DC_MSG_ID_LAST_SPECIAL; use deltachat::contact::Contact; use deltachat::context::Context; use deltachat::dc_tools::{ as_path, dc_strdup, to_opt_string_lossy, to_string_lossy, OsStrExt, StrExt, }; +use deltachat::message::MsgId; use deltachat::stock::StockMessage; use deltachat::*; @@ -141,9 +143,12 @@ impl ContextWrapper { | Event::IncomingMsg { chat_id, msg_id } | Event::MsgDelivered { chat_id, msg_id } | Event::MsgFailed { chat_id, msg_id } - | Event::MsgRead { chat_id, msg_id } => { - ffi_cb(self, event_id, chat_id as uintptr_t, msg_id as uintptr_t) - } + | Event::MsgRead { chat_id, msg_id } => ffi_cb( + self, + event_id, + chat_id as uintptr_t, + msg_id.to_u32() as uintptr_t, + ), Event::ChatModified(chat_id) => ffi_cb(self, event_id, chat_id as uintptr_t, 0), Event::ContactsChanged(id) | Event::LocationChanged(id) => { let id = id.unwrap_or_default(); @@ -681,7 +686,8 @@ pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, ms let ffi_context = &*context; ffi_context .with_inner(|ctx| { - chat::create_by_msg_id(ctx, msg_id).unwrap_or_log_default(ctx, "Failed to create chat") + chat::create_by_msg_id(ctx, MsgId::new(msg_id)) + .unwrap_or_log_default(ctx, "Failed to create chat") }) .unwrap_or(0) } @@ -736,6 +742,7 @@ pub unsafe extern "C" fn dc_prepare_msg( chat::prepare_msg(ctx, chat_id, &mut ffi_msg.message) .unwrap_or_log_default(ctx, "Failed to prepare message") }) + .map(|msg_id| msg_id.to_u32()) .unwrap_or(0) } @@ -756,6 +763,7 @@ pub unsafe extern "C" fn dc_send_msg( chat::send_msg(ctx, chat_id, &mut ffi_msg.message) .unwrap_or_log_default(ctx, "Failed to send message") }) + .map(|msg_id| msg_id.to_u32()) .unwrap_or(0) } @@ -774,6 +782,7 @@ pub unsafe extern "C" fn dc_send_text_msg( ffi_context .with_inner(|ctx| { chat::send_text_msg(ctx, chat_id, text_to_send) + .map(|msg_id| msg_id.to_u32()) .unwrap_or_log_default(ctx, "Failed to send text message") }) .unwrap_or(0) @@ -838,9 +847,19 @@ pub unsafe extern "C" fn dc_get_chat_msgs( return ptr::null_mut(); } let ffi_context = &*context; + let marker_flag = if marker1before <= DC_MSG_ID_LAST_SPECIAL { + None + } else { + Some(MsgId::new(marker1before)) + }; ffi_context .with_inner(|ctx| { - let arr = dc_array_t::from(chat::get_chat_msgs(ctx, chat_id, flags, marker1before)); + let arr = dc_array_t::from( + chat::get_chat_msgs(ctx, chat_id, flags, marker_flag) + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); Box::into_raw(Box::new(arr)) }) .unwrap_or_else(|_| ptr::null_mut()) @@ -884,7 +903,12 @@ pub unsafe extern "C" fn dc_get_fresh_msgs( let ffi_context = &*context; ffi_context .with_inner(|ctx| { - let arr = dc_array_t::from(ctx.get_fresh_msgs()); + let arr = dc_array_t::from( + ctx.get_fresh_msgs() + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); Box::into_raw(Box::new(arr)) }) .unwrap_or_else(|_| ptr::null_mut()) @@ -946,13 +970,12 @@ pub unsafe extern "C" fn dc_get_chat_media( from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3)); ffi_context .with_inner(|ctx| { - let arr = dc_array_t::from(chat::get_chat_media( - ctx, - chat_id, - msg_type, - or_msg_type2, - or_msg_type3, - )); + let arr = dc_array_t::from( + chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3) + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); Box::into_raw(Box::new(arr)) }) .unwrap_or_else(|_| ptr::null_mut()) @@ -985,7 +1008,16 @@ pub unsafe extern "C" fn dc_get_next_media( from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3)); ffi_context .with_inner(|ctx| { - chat::get_next_media(ctx, msg_id, direction, msg_type, or_msg_type2, or_msg_type3) + chat::get_next_media( + ctx, + MsgId::new(msg_id), + direction, + msg_type, + or_msg_type2, + or_msg_type3, + ) + .map(|msg_id| msg_id.to_u32()) + .unwrap_or(0) }) .unwrap_or(0) } @@ -1056,7 +1088,12 @@ pub unsafe extern "C" fn dc_search_msgs( let ffi_context = &*context; ffi_context .with_inner(|ctx| { - let arr = dc_array_t::from(ctx.search_msgs(chat_id, to_string_lossy(query))); + let arr = dc_array_t::from( + ctx.search_msgs(chat_id, to_string_lossy(query)) + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); Box::into_raw(Box::new(arr)) }) .unwrap_or_else(|_| ptr::null_mut()) @@ -1208,7 +1245,7 @@ pub unsafe extern "C" fn dc_get_msg_info( } let ffi_context = &*context; ffi_context - .with_inner(|ctx| message::get_msg_info(ctx, msg_id).strdup()) + .with_inner(|ctx| message::get_msg_info(ctx, MsgId::new(msg_id)).strdup()) .unwrap_or_else(|_| ptr::null_mut()) } @@ -1224,7 +1261,7 @@ pub unsafe extern "C" fn dc_get_mime_headers( let ffi_context = &*context; ffi_context .with_inner(|ctx| { - message::get_mime_headers(ctx, msg_id) + message::get_mime_headers(ctx, MsgId::new(msg_id)) .map(|s| s.strdup()) .unwrap_or_else(|| ptr::null_mut()) }) @@ -1242,11 +1279,10 @@ pub unsafe extern "C" fn dc_delete_msgs( return; } let ffi_context = &*context; - let ids = std::slice::from_raw_parts(msg_ids, msg_cnt as usize); - + let msg_ids: Vec = ids.iter().map(|id| MsgId::new(*id)).collect(); ffi_context - .with_inner(|ctx| message::delete_msgs(ctx, ids)) + .with_inner(|ctx| message::delete_msgs(ctx, &msg_ids[..])) .unwrap_or(()) } @@ -1266,11 +1302,11 @@ pub unsafe extern "C" fn dc_forward_msgs( return; } let ids = std::slice::from_raw_parts(msg_ids, msg_cnt as usize); - + let msg_ids: Vec = ids.iter().map(|id| MsgId::new(*id)).collect(); let ffi_context = &*context; ffi_context .with_inner(|ctx| { - chat::forward_msgs(ctx, ids, chat_id) + chat::forward_msgs(ctx, &msg_ids[..], chat_id) .unwrap_or_log_default(ctx, "Failed to forward message") }) .unwrap_or_default() @@ -1299,10 +1335,14 @@ pub unsafe extern "C" fn dc_markseen_msgs( return; } let ids = std::slice::from_raw_parts(msg_ids, msg_cnt as usize); - + let msg_ids: Vec = ids + .iter() + .filter(|id| **id > DC_MSG_ID_LAST_SPECIAL) + .map(|id| MsgId::new(*id)) + .collect(); let ffi_context = &*context; ffi_context - .with_inner(|ctx| message::markseen_msgs(ctx, ids)) + .with_inner(|ctx| message::markseen_msgs(ctx, &msg_ids[..])) .ok(); } @@ -1317,12 +1357,11 @@ pub unsafe extern "C" fn dc_star_msgs( eprintln!("ignoring careless call to dc_star_msgs()"); return; } - let ids = std::slice::from_raw_parts(msg_ids, msg_cnt as usize); - + let msg_ids: Vec = ids.iter().map(|id| MsgId::new(*id)).collect(); let ffi_context = &*context; ffi_context - .with_inner(|ctx| message::star_msgs(ctx, ids, star == 1)) + .with_inner(|ctx| message::star_msgs(ctx, &msg_ids[..], star == 1)) .ok(); } @@ -1335,7 +1374,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) -> let ffi_context = &*context; ffi_context .with_inner(|ctx| { - let message = match message::Message::load_from_db(ctx, msg_id) { + let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)) { Ok(msg) => msg, Err(e) => { error!(ctx, "Error getting msg #{}: {}", msg_id, e); @@ -1622,7 +1661,8 @@ pub unsafe extern "C" fn dc_continue_key_transfer( let ffi_context = &*context; ffi_context .with_inner(|ctx| { - match imex::continue_key_transfer(ctx, msg_id, &to_string_lossy(setup_code)) { + match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code)) + { Ok(()) => 1, Err(err) => { error!(ctx, "dc_continue_key_transfer: {}", err); @@ -2033,7 +2073,11 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id( return 0; } let ffi_list = &*chatlist; - ffi_list.list.get_msg_id(index as usize) + ffi_list + .list + .get_msg_id(index as usize) + .map(|msg_id| msg_id.to_u32()) + .unwrap_or(0) } #[no_mangle] @@ -2275,7 +2319,7 @@ pub unsafe extern "C" fn dc_msg_get_id(msg: *mut dc_msg_t) -> u32 { return 0; } let ffi_msg = &*msg; - ffi_msg.message.get_id() + ffi_msg.message.get_id().to_u32() } #[no_mangle] diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index e32878e0b..527c1ab24 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -14,7 +14,7 @@ use deltachat::imex::*; use deltachat::job::*; use deltachat::location; use deltachat::lot::LotState; -use deltachat::message::{self, Message, MessageState}; +use deltachat::message::{self, Message, MessageState, MsgId}; use deltachat::peerstate::*; use deltachat::qr::*; use deltachat::sql; @@ -86,7 +86,7 @@ pub unsafe fn dc_reset_tables(context: &Context, bits: i32) -> i32 { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); 1 @@ -170,7 +170,7 @@ fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int { if read_cnt > 0 { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); } 1 @@ -194,7 +194,7 @@ unsafe fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { context, "{}#{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]", prefix.as_ref(), - msg.get_id() as libc::c_int, + msg.get_id(), if msg.get_showpadlock() { "🔒" } else { "" }, if msg.has_location() { "📍" } else { "" }, &contact_name, @@ -221,17 +221,17 @@ unsafe fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { ); } -unsafe fn log_msglist(context: &Context, msglist: &Vec) -> Result<(), Error> { +unsafe fn log_msglist(context: &Context, msglist: &Vec) -> Result<(), Error> { let mut lines_out = 0; for &msg_id in msglist { - if msg_id == 9 as libc::c_uint { + if msg_id.is_daymarker() { info!( context, "--------------------------------------------------------------------------------" ); lines_out += 1 - } else if msg_id > 0 { + } else if !msg_id.is_special() { if lines_out == 0 { info!( context, @@ -418,7 +418,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E }, "get-setupcodebegin" => { ensure!(!arg1.is_empty(), "Argument missing."); - let msg_id: u32 = arg1.parse()?; + let msg_id: MsgId = MsgId::new(arg1.parse()?); let msg = Message::load_from_db(context, msg_id)?; if msg.is_setupmessage() { let setupcodebegin = msg.get_setupcodebegin(context); @@ -436,7 +436,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E !arg1.is_empty() && !arg2.is_empty(), "Arguments expected" ); - continue_key_transfer(context, arg1.parse()?, &arg2)?; + continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?; } "has-backup" => { has_backup(context, blobdir)?; @@ -581,7 +581,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E ensure!(sel_chat.is_some(), "Failed to select chat"); let sel_chat = sel_chat.as_ref().unwrap(); - let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, 0); + let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None); let temp2 = sel_chat.get_subtitle(context); let temp_name = sel_chat.get_name(); info!( @@ -617,7 +617,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E } "createchatbymsg" => { ensure!(!arg1.is_empty(), "Argument missing"); - let msg_id: u32 = arg1.parse()?; + let msg_id = MsgId::new(arg1.parse()?); let chat_id = chat::create_by_msg_id(context, msg_id)?; let chat = Chat::load_from_db(context, chat_id)?; @@ -849,7 +849,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E } "msginfo" => { ensure!(!arg1.is_empty(), "Argument missing."); - let id = arg1.parse()?; + let id = MsgId::new(arg1.parse()?); let res = message::get_msg_info(context, id); println!("{}", res); } @@ -865,27 +865,27 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E "Arguments expected" ); - let mut msg_ids = [0; 1]; + let mut msg_ids = [MsgId::new(0); 1]; let chat_id = arg2.parse()?; - msg_ids[0] = arg1.parse()?; + msg_ids[0] = MsgId::new(arg1.parse()?); chat::forward_msgs(context, &msg_ids, chat_id)?; } "markseen" => { ensure!(!arg1.is_empty(), "Argument missing."); - let mut msg_ids = [0; 1]; - msg_ids[0] = arg1.parse()?; + let mut msg_ids = [MsgId::new(0); 1]; + msg_ids[0] = MsgId::new(arg1.parse()?); message::markseen_msgs(context, &msg_ids); } "star" | "unstar" => { ensure!(!arg1.is_empty(), "Argument missing."); - let mut msg_ids = [0; 1]; - msg_ids[0] = arg1.parse()?; + let mut msg_ids = [MsgId::new(0); 1]; + msg_ids[0] = MsgId::new(arg1.parse()?); message::star_msgs(context, &msg_ids, arg0 == "star"); } "delmsg" => { ensure!(!arg1.is_empty(), "Argument missing."); - let mut ids = [0; 1]; - ids[0] = arg1.parse()?; + let mut ids = [MsgId::new(0); 1]; + ids[0] = MsgId::new(arg1.parse()?); message::delete_msgs(context, &ids); } "listcontacts" | "contacts" | "listverified" => { diff --git a/src/blob.rs b/src/blob.rs index 03e2da572..a01432216 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -193,6 +193,8 @@ impl<'a> BlobObject<'a> { /// the database. Do not use this unless you're about to store /// this string in the database or [Params]. Eventually even /// those conversions should be handled by the type system. + /// + /// [Params]: crate::param::Params pub fn as_name(&self) -> &str { &self.name } diff --git a/src/chat.rs b/src/chat.rs index 6581c49cc..9f5ce4663 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use itertools::Itertools; + use crate::blob::{BlobErrorKind, BlobObject}; use crate::chatlist::*; use crate::config::*; @@ -11,7 +13,7 @@ use crate::dc_tools::*; use crate::error::Error; use crate::events::Event; use crate::job::*; -use crate::message::{self, Message, MessageState}; +use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId}; use crate::param::*; use crate::sql::{self, Sql}; use crate::stock::StockMessage; @@ -238,7 +240,7 @@ impl Chat { context: &Context, msg: &mut Message, timestamp: i64, - ) -> Result { + ) -> Result { let mut do_guarantee_e2ee: bool; let e2ee_enabled: bool; let mut new_references = "".into(); @@ -252,7 +254,7 @@ impl Chat { || self.typ == Chattype::VerifiedGroup) { error!(context, "Cannot send to chat type #{}.", self.typ,); - return Ok(0); + bail!("Cannot set to chat type #{}", self.typ); } if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup) @@ -262,7 +264,7 @@ impl Chat { context, Event::ErrorSelfNotInGroup("Cannot send message; self not in group.".into()) ); - return Ok(0); + bail!("Cannot set message; self not in group."); } if let Some(from) = context.get_config(Config::ConfiguredAddr) { @@ -286,7 +288,10 @@ impl Chat { context, "Cannot send message, contact for chat #{} not found.", self.id, ); - return Ok(0); + bail!( + "Cannot set message, contact for chat #{} not found.", + self.id + ); } } else { if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup { @@ -472,7 +477,7 @@ impl Chat { error!(context, "Cannot send message, not configured.",); } - Ok(msg_id) + Ok(MsgId::new(msg_id)) } } @@ -494,7 +499,7 @@ impl Chat { /// to the message and, depending on the contacts origin, messages from the /// same group may be shown or not - so, all in all, it is fine to show the /// contact name only. -pub fn create_by_msg_id(context: &Context, msg_id: u32) -> Result { +pub fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result { let mut chat_id = 0; let mut send_event = false; @@ -514,7 +519,7 @@ pub fn create_by_msg_id(context: &Context, msg_id: u32) -> Result { if send_event { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); } @@ -557,7 +562,7 @@ pub fn create_by_contact_id(context: &Context, contact_id: u32) -> Result( context: &'a Context, chat_id: u32, msg: &mut Message, -) -> Result { +) -> Result { ensure!( chat_id > DC_CHAT_ID_LAST_SPECIAL, "Cannot prepare message for special chat" @@ -672,8 +677,8 @@ pub fn msgtype_has_file(msgtype: Viewtype) -> bool { } } -fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Result { - msg.id = 0; +fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Result { + msg.id = MsgId::new_unset(); if msg.type_0 == Viewtype::Text { // the caller should check if the message text is empty } else if msgtype_has_file(msg.type_0) { @@ -779,7 +784,7 @@ pub fn unarchive(context: &Context, chat_id: u32) -> Result<(), Error> { /// However, this does not imply, the message really reached the recipient - /// sending may be delayed eg. due to network problems. However, from your /// view, you're done with the message. Sooner or later it will find its way. -pub fn send_msg(context: &Context, chat_id: u32, msg: &mut Message) -> Result { +pub fn send_msg(context: &Context, chat_id: u32, msg: &mut Message) -> Result { // dc_prepare_msg() leaves the message state to OutPreparing, we // only have to change the state to OutPending in this case. // Otherwise we still have to prepare the message, which will set @@ -811,12 +816,17 @@ pub fn send_msg(context: &Context, chat_id: u32, msg: &mut Message) -> Result() + .map_err(|_| InvalidMsgId) + .map(|id| MsgId::new(id)) + { + Ok(msg_id) => { + if let Ok(mut msg) = Message::load_from_db(context, msg_id) { + send_msg(context, 0, &mut msg)?; + }; + } + Err(_) => (), } } msg.param.remove(Param::PrepForwards); @@ -827,7 +837,11 @@ pub fn send_msg(context: &Context, chat_id: u32, msg: &mut Message) -> Result Result { +pub fn send_text_msg( + context: &Context, + chat_id: u32, + text_to_send: String, +) -> Result { ensure!( chat_id > DC_CHAT_ID_LAST_SPECIAL, "bad chat_id = {} <= 9", @@ -851,7 +865,10 @@ pub fn set_draft(context: &Context, chat_id: u32, msg: Option<&mut Message>) { }; if changed { - context.call_cb(Event::MsgsChanged { chat_id, msg_id: 0 }); + context.call_cb(Event::MsgsChanged { + chat_id, + msg_id: MsgId::new(0), + }); } } @@ -859,12 +876,13 @@ pub fn set_draft(context: &Context, chat_id: u32, msg: Option<&mut Message>) { /// /// Return {true}, if message was deleted, {false} otherwise. fn maybe_delete_draft(context: &Context, chat_id: u32) -> bool { - let draft = get_draft_msg_id(context, chat_id); - if draft != 0 { - Message::delete_from_db(context, draft); - return true; + match get_draft_msg_id(context, chat_id) { + Some(msg_id) => { + Message::delete_from_db(context, msg_id); + true + } + None => false, } - false } /// Set provided message as draft message for specified chat. @@ -916,97 +934,113 @@ fn set_draft_raw(context: &Context, chat_id: u32, msg: &mut Message) -> bool { deleted || set } -fn get_draft_msg_id(context: &Context, chat_id: u32) -> u32 { - context - .sql - .query_get_value::<_, i32>( - context, - "SELECT id FROM msgs WHERE chat_id=? AND state=?;", - params![chat_id as i32, MessageState::OutDraft], - ) - .unwrap_or_default() as u32 +fn get_draft_msg_id(context: &Context, chat_id: u32) -> Option { + context.sql.query_get_value::<_, MsgId>( + context, + "SELECT id FROM msgs WHERE chat_id=? AND state=?;", + params![chat_id as i32, MessageState::OutDraft], + ) } pub fn get_draft(context: &Context, chat_id: u32) -> Result, Error> { if chat_id <= DC_CHAT_ID_LAST_SPECIAL { return Ok(None); } - let draft_msg_id = get_draft_msg_id(context, chat_id); - if draft_msg_id == 0 { - return Ok(None); + match get_draft_msg_id(context, chat_id) { + Some(draft_msg_id) => Ok(Some(Message::load_from_db(context, draft_msg_id)?)), + None => Ok(None), } - Ok(Some(Message::load_from_db(context, draft_msg_id)?)) } -pub fn get_chat_msgs(context: &Context, chat_id: u32, flags: u32, marker1before: u32) -> Vec { - let mut ret = Vec::new(); - - let mut last_day = 0; - let cnv_to_local = dc_gm2local_offset(); - - let process_row = |row: &rusqlite::Row| Ok((row.get::<_, i32>(0)?, row.get::<_, i64>(1)?)); +pub fn get_chat_msgs( + context: &Context, + chat_id: u32, + flags: u32, + marker1before: Option, +) -> Vec { + let process_row = + |row: &rusqlite::Row| Ok((row.get::<_, MsgId>("id")?, row.get::<_, i64>("timestamp")?)); let process_rows = |rows: rusqlite::MappedRows<_>| { + let mut ret = Vec::new(); + let mut last_day = 0; + let cnv_to_local = dc_gm2local_offset(); for row in rows { let (curr_id, ts) = row?; - if curr_id as u32 == marker1before { - ret.push(DC_MSG_ID_MARKER1); + if let Some(marker_id) = marker1before { + if curr_id == marker_id { + ret.push(MsgId::new(DC_MSG_ID_MARKER1)); + } } - if 0 != flags & 0x1 { + if (flags & DC_GCM_ADDDAYMARKER) != 0 { let curr_local_timestamp = ts + cnv_to_local; let curr_day = curr_local_timestamp / 86400; if curr_day != last_day { - ret.push(DC_MSG_ID_LAST_SPECIAL); + ret.push(MsgId::new(DC_MSG_ID_DAYMARKER)); last_day = curr_day; } } - ret.push(curr_id as u32); + ret.push(curr_id); } - Ok(()) + Ok(ret) }; - - let success = if chat_id == 1 { + let success = if chat_id == DC_CHAT_ID_DEADDROP { let show_emails = context.get_config_int(Config::ShowEmails); context.sql.query_map( - "SELECT m.id, m.timestamp FROM msgs m \ - LEFT JOIN chats ON m.chat_id=chats.id \ - LEFT JOIN contacts ON m.from_id=contacts.id WHERE m.from_id!=1 \ - AND m.from_id!=2 \ - AND m.hidden=0 \ - AND chats.blocked=2 \ - AND contacts.blocked=0 \ - AND m.msgrmsg>=? \ - ORDER BY m.timestamp,m.id;", + concat!( + "SELECT m.id AS id, m.timestamp AS timestamp", + " FROM msgs m", + " LEFT JOIN chats", + " ON m.chat_id=chats.id", + " LEFT JOIN contacts", + " ON m.from_id=contacts.id", + " WHERE m.from_id!=1", + " AND m.from_id!=2", + " AND m.hidden=0", + " AND chats.blocked=2", + " AND contacts.blocked=0", + " AND m.msgrmsg>=?", + " ORDER BY m.timestamp,m.id;" + ), params![if show_emails == 2 { 0 } else { 1 }], process_row, process_rows, ) - } else if chat_id == 5 { + } else if chat_id == DC_CHAT_ID_STARRED { context.sql.query_map( - "SELECT m.id, m.timestamp FROM msgs m \ - LEFT JOIN contacts ct ON m.from_id=ct.id WHERE m.starred=1 \ - AND m.hidden=0 \ - AND ct.blocked=0 \ - ORDER BY m.timestamp,m.id;", + concat!( + "SELECT m.id AS id, m.timestamp AS timestamp", + " FROM msgs m", + " LEFT JOIN contacts ct", + " ON m.from_id=ct.id", + " WHERE m.starred=1", + " AND m.hidden=0", + " AND ct.blocked=0", + " ORDER BY m.timestamp,m.id;" + ), params![], process_row, process_rows, ) } else { context.sql.query_map( - "SELECT m.id, m.timestamp FROM msgs m \ - WHERE m.chat_id=? \ - AND m.hidden=0 \ - ORDER BY m.timestamp,m.id;", + concat!( + "SELECT m.id AS id, m.timestamp AS timestamp", + " FROM msgs m", + " WHERE m.chat_id=?", + " AND m.hidden=0", + " ORDER BY m.timestamp, m.id;" + ), params![chat_id as i32], process_row, process_rows, ) }; - - if success.is_ok() { - ret - } else { - Vec::new() + match success { + Ok(ret) => ret, + Err(e) => { + error!(context, "Failed to get chat messages: {}", e); + Vec::new() + } } } @@ -1053,7 +1087,7 @@ pub fn marknoticed_chat(context: &Context, chat_id: u32) -> Result<(), Error> { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); Ok(()) @@ -1077,7 +1111,7 @@ pub fn marknoticed_all_chats(context: &Context) -> Result<(), Error> { )?; context.call_cb(Event::MsgsChanged { - msg_id: 0, + msg_id: MsgId::new(0), chat_id: 0, }); @@ -1090,31 +1124,44 @@ pub fn get_chat_media( msg_type: Viewtype, msg_type2: Viewtype, msg_type3: Viewtype, -) -> Vec { - context.sql.query_map( - "SELECT id FROM msgs WHERE chat_id=? AND (type=? OR type=? OR type=?) ORDER BY timestamp, id;", - params![ - chat_id as i32, - msg_type, - if msg_type2 != Viewtype::Unknown { - msg_type2 - } else { - msg_type - }, if msg_type3 != Viewtype::Unknown { - msg_type3 - } else { - msg_type +) -> Vec { + context + .sql + .query_map( + concat!( + "SELECT", + " id", + " FROM msgs", + " WHERE chat_id=? AND (type=? OR type=? OR type=?)", + " ORDER BY timestamp, id;" + ), + params![ + chat_id as i32, + msg_type, + if msg_type2 != Viewtype::Unknown { + msg_type2 + } else { + msg_type + }, + if msg_type3 != Viewtype::Unknown { + msg_type3 + } else { + msg_type + }, + ], + |row| row.get::<_, MsgId>(0), + |ids| { + let mut ret = Vec::new(); + for id in ids { + match id { + Ok(msg_id) => ret.push(msg_id), + Err(_) => (), + } + } + Ok(ret) }, - ], - |row| row.get::<_, i32>(0), - |ids| { - let mut ret = Vec::new(); - for id in ids { - ret.push(id? as u32); - } - Ok(ret) - } - ).unwrap_or_default() + ) + .unwrap_or_default() } /// Indicates the direction over which to iterate. @@ -1127,16 +1174,16 @@ pub enum Direction { pub fn get_next_media( context: &Context, - curr_msg_id: u32, + curr_msg_id: MsgId, direction: Direction, msg_type: Viewtype, msg_type2: Viewtype, msg_type3: Viewtype, -) -> u32 { - let mut ret = 0; +) -> Option { + let mut ret: Option = None; if let Ok(msg) = Message::load_from_db(context, curr_msg_id) { - let list = get_chat_media( + let list: Vec = get_chat_media( context, msg.chat_id, if msg_type != Viewtype::Unknown { @@ -1152,12 +1199,12 @@ pub fn get_next_media( match direction { Direction::Forward => { if i + 1 < list.len() { - ret = list[i + 1] + ret = Some(list[i + 1]); } } Direction::Backward => { if i >= 1 { - ret = list[i - 1]; + ret = Some(list[i - 1]); } } } @@ -1196,7 +1243,7 @@ pub fn archive(context: &Context, chat_id: u32, archive: bool) -> Result<(), Err )?; context.call_cb(Event::MsgsChanged { - msg_id: 0, + msg_id: MsgId::new(0), chat_id: 0, }); @@ -1241,7 +1288,7 @@ pub fn delete(context: &Context, chat_id: u32) -> Result<(), Error> { )?; context.call_cb(Event::MsgsChanged { - msg_id: 0, + msg_id: MsgId::new(0), chat_id: 0, }); @@ -1310,7 +1357,7 @@ pub fn create_group_chat( } context.call_cb(Event::MsgsChanged { - msg_id: 0, + msg_id: MsgId::new(0), chat_id: 0, }); } @@ -1425,10 +1472,13 @@ pub(crate) fn add_contact_to_chat_ex( msg.id = send_msg(context, chat_id, &mut msg)?; context.call_cb(Event::MsgsChanged { chat_id, - msg_id: msg.id, + msg_id: MsgId::from(msg.id), }); } - context.call_cb(Event::MsgsChanged { chat_id, msg_id: 0 }); + context.call_cb(Event::MsgsChanged { + chat_id, + msg_id: MsgId::new(0), + }); Ok(true) } @@ -1719,39 +1769,33 @@ pub fn set_chat_profile_image( Ok(()) } -pub fn forward_msgs(context: &Context, msg_ids: &[u32], chat_id: u32) -> Result<(), Error> { - ensure!(!msg_ids.is_empty(), "empty msgs_ids: no one to forward to"); +pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: u32) -> Result<(), Error> { + ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward"); ensure!( chat_id > DC_CHAT_ID_LAST_SPECIAL, "can not forward to special chat" ); - let mut created_db_entries = Vec::new(); + let mut created_chats: Vec = Vec::new(); + let mut created_msgs: Vec = Vec::new(); let mut curr_timestamp: i64; unarchive(context, chat_id)?; if let Ok(mut chat) = Chat::load_from_db(context, chat_id) { curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()); - let idsstr = msg_ids - .iter() - .enumerate() - .fold(String::with_capacity(2 * msg_ids.len()), |acc, (i, n)| { - (if i == 0 { acc } else { acc + "," }) + &n.to_string() - }); - let ids = context.sql.query_map( format!( "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", - idsstr + msg_ids.iter().map(|_| "?").join(",") ), - params![], - |row| row.get::<_, i32>(0), + msg_ids, + |row| row.get::<_, MsgId>(0), |ids| ids.collect::, _>>().map_err(Into::into), )?; for id in ids { - let src_msg_id = id; - let msg = Message::load_from_db(context, src_msg_id as u32); + let src_msg_id: MsgId = id; + let msg = Message::load_from_db(context, src_msg_id); if msg.is_err() { break; } @@ -1767,22 +1811,21 @@ pub fn forward_msgs(context: &Context, msg_ids: &[u32], chat_id: u32) -> Result< msg.param.remove(Param::ForcePlaintext); msg.param.remove(Param::Cmd); - let new_msg_id: u32; + let new_msg_id: MsgId; if msg.state == MessageState::OutPreparing { let fresh9 = curr_timestamp; curr_timestamp += 1; - new_msg_id = chat - .prepare_msg_raw(context, &mut msg, fresh9) - .unwrap_or_default(); + new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh9)?; let save_param = msg.param.clone(); msg.param = original_param; - msg.id = src_msg_id as u32; + msg.id = src_msg_id; if let Some(old_fwd) = msg.param.get(Param::PrepForwards) { - let new_fwd = format!("{} {}", old_fwd, new_msg_id); + let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32()); msg.param.set(Param::PrepForwards, new_fwd); } else { - msg.param.set(Param::PrepForwards, new_msg_id.to_string()); + msg.param + .set(Param::PrepForwards, new_msg_id.to_u32().to_string()); } msg.save_param_to_disk(context); @@ -1791,23 +1834,19 @@ pub fn forward_msgs(context: &Context, msg_ids: &[u32], chat_id: u32) -> Result< msg.state = MessageState::OutPending; let fresh10 = curr_timestamp; curr_timestamp += 1; - new_msg_id = chat - .prepare_msg_raw(context, &mut msg, fresh10) - .unwrap_or_default(); + new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh10)?; job_send_msg(context, new_msg_id)?; } - created_db_entries.push(chat_id); - created_db_entries.push(new_msg_id); + created_chats.push(chat_id); + created_msgs.push(new_msg_id); } } - - for i in (0..created_db_entries.len()).step_by(2) { + for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) { context.call_cb(Event::MsgsChanged { - chat_id: created_db_entries[i], - msg_id: created_db_entries[i + 1], + chat_id: *chat_id, + msg_id: *msg_id, }); } - Ok(()) } @@ -1874,8 +1913,11 @@ pub fn add_device_msg(context: &Context, chat_id: u32, text: impl AsRef) { return; } - let msg_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); - context.call_cb(Event::MsgsChanged { chat_id, msg_id }); + let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); + context.call_cb(Event::MsgsChanged { + chat_id, + msg_id: MsgId::new(row_id), + }); } #[cfg(test)] diff --git a/src/chatlist.rs b/src/chatlist.rs index b23662a01..e0a32501f 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -4,7 +4,7 @@ use crate::contact::*; use crate::context::*; use crate::error::Result; use crate::lot::Lot; -use crate::message::Message; +use crate::message::{Message, MsgId}; use crate::stock::StockMessage; /// An object representing a single chatlist in memory. @@ -34,7 +34,7 @@ use crate::stock::StockMessage; #[derive(Debug)] pub struct Chatlist { /// Stores pairs of `chat_id, message_id` - ids: Vec<(u32, u32)>, + ids: Vec<(u32, MsgId)>, } impl Chatlist { @@ -86,25 +86,12 @@ impl Chatlist { query: Option<&str>, query_contact_id: Option, ) -> Result { - let mut add_archived_link_item = 0; - - // select with left join and minimum: - // - the inner select must use `hidden` and _not_ `m.hidden` - // which would refer the outer select and take a lot of time - // - `GROUP BY` is needed several messages may have the same timestamp - // - the list starts with the newest chats - // nb: the query currently shows messages from blocked contacts in groups. - // however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs() - // (otherwise it would be hard to follow conversations, wa and tg do the same) - // for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not - // shown at all permanent in the chatlist. + let mut add_archived_link_item = false; let process_row = |row: &rusqlite::Row| { - let chat_id: i32 = row.get(0)?; - // TODO: verify that it is okay for this to be Null - let msg_id: i32 = row.get(1).unwrap_or_default(); - - Ok((chat_id as u32, msg_id as u32)) + let chat_id: u32 = row.get(0)?; + let msg_id: MsgId = row.get(1).unwrap_or_default(); + Ok((chat_id, msg_id)) }; let process_rows = |rows: rusqlite::MappedRows<_>| { @@ -112,36 +99,63 @@ impl Chatlist { .map_err(Into::into) }; - // nb: the query currently shows messages from blocked contacts in groups. - // however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs() - // (otherwise it would be hard to follow conversations, wa and tg do the same) - // for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not + // select with left join and minimum: + // + // - the inner select must use `hidden` and _not_ `m.hidden` + // which would refer the outer select and take a lot of time + // - `GROUP BY` is needed several messages may have the same + // timestamp + // - the list starts with the newest chats + // + // nb: the query currently shows messages from blocked + // contacts in groups. however, for normal-groups, this is + // okay as the message is also returned by dc_get_chat_msgs() + // (otherwise it would be hard to follow conversations, wa and + // tg do the same) for the deaddrop, however, they should + // really be hidden, however, _currently_ the deaddrop is not // shown at all permanent in the chatlist. - let mut ids = if let Some(query_contact_id) = query_contact_id { // show chats shared with a given contact context.sql.query_map( - "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?) \ - GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", - params![query_contact_id as i32], - process_row, - process_rows, - )? + concat!( + "SELECT c.id, m.id", + " FROM chats c", + " LEFT JOIN msgs m", + " ON c.id=m.chat_id", + " AND m.timestamp=(", + " SELECT MAX(timestamp)", + " FROM msgs", + " WHERE chat_id=c.id", + " AND (hidden=0 OR (hidden=1 AND state=19)))", + " WHERE c.id>9", + " AND c.blocked=0", + " AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", + " GROUP BY c.id", + " ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;" + ), + params![query_contact_id as i32], + process_row, + process_rows, + )? } else if 0 != listflags & DC_GCL_ARCHIVED_ONLY { // show archived chats context.sql.query_map( - "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.archived=1 GROUP BY c.id \ - ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + concat!( + "SELECT c.id, m.id", + " FROM chats c", + " LEFT JOIN msgs m", + " ON c.id=m.chat_id", + " AND m.timestamp=(", + " SELECT MAX(timestamp)", + " FROM msgs", + " WHERE chat_id=c.id", + " AND (hidden=0 OR (hidden=1 AND state=19)))", + " WHERE c.id>9", + " AND c.blocked=0", + " AND c.archived=1", + " GROUP BY c.id", + " ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;" + ), params![], process_row, process_rows, @@ -152,13 +166,22 @@ impl Chatlist { let str_like_cmd = format!("%{}%", query); context.sql.query_map( - "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.name LIKE ? \ - GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + concat!( + "SELECT c.id, m.id", + " FROM chats c", + " LEFT JOIN msgs m", + " ON c.id=m.chat_id", + " AND m.timestamp=(", + " SELECT MAX(timestamp)", + " FROM msgs", + " WHERE chat_id=c.id", + " AND (hidden=0 OR (hidden=1 AND state=19)))", + " WHERE c.id>9", + " AND c.blocked=0", + " AND c.name LIKE ?", + " GROUP BY c.id", + " ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;" + ), params![str_like_cmd], process_row, process_rows, @@ -166,34 +189,40 @@ impl Chatlist { } else { // show normal chatlist let mut ids = context.sql.query_map( - "SELECT c.id, m.id FROM chats c \ - LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.archived=0 \ - GROUP BY c.id \ - ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + concat!( + "SELECT c.id, m.id", + " FROM chats c", + " LEFT JOIN msgs m", + " ON c.id=m.chat_id", + " AND m.timestamp=(", + " SELECT MAX(timestamp)", + " FROM msgs", + " WHERE chat_id=c.id", + " AND (hidden=0 OR (hidden=1 AND state=19)))", + " WHERE c.id>9", + " AND c.blocked=0", + " AND c.archived=0", + " GROUP BY c.id", + " ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;" + ), params![], process_row, process_rows, )?; if 0 == listflags & DC_GCL_NO_SPECIALS { - let last_deaddrop_fresh_msg_id = get_last_deaddrop_fresh_msg(context); - if last_deaddrop_fresh_msg_id > 0 { + if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) { ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id)); } - add_archived_link_item = 1; + add_archived_link_item = true; } ids }; - if 0 != add_archived_link_item && dc_get_archived_cnt(context) > 0 { + if add_archived_link_item && dc_get_archived_cnt(context) > 0 { if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT { - ids.push((DC_CHAT_ID_ALLDONE_HINT, 0)); + ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0))); } - ids.push((DC_CHAT_ID_ARCHIVED_LINK, 0)); + ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0))); } Ok(Chatlist { ids }) @@ -221,12 +250,9 @@ impl Chatlist { /// Get a single message ID of a chatlist. /// /// To get the message object from the message ID, use dc_get_msg(). - pub fn get_msg_id(&self, index: usize) -> u32 { - if index >= self.ids.len() { - return 0; - } - - self.ids[index].1 + pub fn get_msg_id(&self, index: usize) -> Result { + ensure!(index >= self.ids.len(), "Chatlist index out of range"); + Ok(self.ids[index].1) } /// Get a summary for a chatlist index. @@ -268,7 +294,7 @@ impl Chatlist { let lastmsg_id = self.ids[index].1; let mut lastcontact = None; - let lastmsg = if 0 != lastmsg_id { + let lastmsg = if !lastmsg_id.is_special() { if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) { if lastmsg.from_id != 1 && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) @@ -308,19 +334,21 @@ pub fn dc_get_archived_cnt(context: &Context) -> u32 { .unwrap_or_default() } -fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 { - // We have an index over the state-column, this should be sufficient as there are typically - // only few fresh messages. - context - .sql - .query_get_value( - context, - "SELECT m.id FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ - WHERE m.state=10 \ - AND m.hidden=0 \ - AND c.blocked=2 \ - ORDER BY m.timestamp DESC, m.id DESC;", - params![], - ) - .unwrap_or_default() +fn get_last_deaddrop_fresh_msg(context: &Context) -> Option { + // We have an index over the state-column, this should be + // sufficient as there are typically only few fresh messages. + context.sql.query_get_value( + context, + concat!( + "SELECT m.id", + " FROM msgs m", + " LEFT JOIN chats c", + " ON c.id=m.chat_id", + " WHERE m.state=10", + " AND m.hidden=0", + " AND c.blocked=2", + " ORDER BY m.timestamp DESC, m.id DESC;" + ), + params![], + ) } diff --git a/src/constants.rs b/src/constants.rs index ec005aeba..a330d29eb 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -68,7 +68,7 @@ pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01; pub const DC_GCL_NO_SPECIALS: usize = 0x02; pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04; -const DC_GCM_ADDDAYMARKER: usize = 0x01; +pub const DC_GCM_ADDDAYMARKER: u32 = 0x01; pub const DC_GCL_VERIFIED_ONLY: usize = 0x01; pub const DC_GCL_ADD_SELF: usize = 0x02; @@ -120,7 +120,7 @@ impl Default for Chattype { } pub const DC_MSG_ID_MARKER1: u32 = 1; -const DC_MSG_ID_DAYMARKER: u32 = 9; +pub const DC_MSG_ID_DAYMARKER: u32 = 9; pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9; /// approx. max. length returned by dc_msg_get_text() diff --git a/src/contact.rs b/src/contact.rs index c9842bce7..22c99d3ad 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -14,7 +14,7 @@ use crate::error::Result; use crate::events::Event; use crate::key::*; use crate::login_param::LoginParam; -use crate::message::MessageState; +use crate::message::{MessageState, MsgId}; use crate::peerstate::*; use crate::sql; use crate::stock::StockMessage; @@ -243,7 +243,7 @@ impl Contact { { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); } } diff --git a/src/context.rs b/src/context.rs index 0e5753151..2ca85e50a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -17,7 +17,7 @@ use crate::job_thread::JobThread; use crate::key::*; use crate::login_param::LoginParam; use crate::lot::Lot; -use crate::message::{self, Message}; +use crate::message::{self, Message, MsgId}; use crate::param::Params; use crate::smtp::*; use crate::sql::Sql; @@ -313,24 +313,30 @@ impl Context { res } - pub fn get_fresh_msgs(&self) -> Vec { + pub fn get_fresh_msgs(&self) -> Vec { let show_deaddrop = 0; - self.sql .query_map( - "SELECT m.id FROM msgs m LEFT JOIN contacts ct \ - ON m.from_id=ct.id LEFT JOIN chats c ON m.chat_id=c.id WHERE m.state=? \ - AND m.hidden=0 \ - AND m.chat_id>? \ - AND ct.blocked=0 \ - AND (c.blocked=0 OR c.blocked=?) ORDER BY m.timestamp DESC,m.id DESC;", + concat!( + "SELECT m.id", + " FROM msgs m", + " LEFT JOIN contacts ct", + " ON m.from_id=ct.id", + " LEFT JOIN chats c", + " ON m.chat_id=c.id", + " WHERE m.state=?", + " AND m.hidden=0", + " AND m.chat_id>?", + " AND ct.blocked=0", + " AND (c.blocked=0 OR c.blocked=?)", + " ORDER BY m.timestamp DESC,m.id DESC;" + ), &[10, 9, if 0 != show_deaddrop { 2 } else { 0 }], - |row| row.get(0), + |row| row.get::<_, MsgId>(0), |rows| { let mut ret = Vec::new(); for row in rows { - let id: u32 = row?; - ret.push(id); + ret.push(row?); } Ok(ret) }, @@ -339,7 +345,7 @@ impl Context { } #[allow(non_snake_case)] - pub fn search_msgs(&self, chat_id: u32, query: impl AsRef) -> Vec { + pub fn search_msgs(&self, chat_id: u32, query: impl AsRef) -> Vec { let real_query = query.as_ref().trim(); if real_query.is_empty() { return Vec::new(); @@ -348,25 +354,43 @@ impl Context { let strLikeBeg = format!("{}%", real_query); let query = if 0 != chat_id { - "SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id WHERE m.chat_id=? \ - AND m.hidden=0 \ - AND ct.blocked=0 AND (txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp,m.id;" + concat!( + "SELECT m.id AS id, m.timestamp AS timestamp", + " FROM msgs m", + " LEFT JOIN contacts ct", + " ON m.from_id=ct.id", + " WHERE m.chat_id=?", + " AND m.hidden=0", + " AND ct.blocked=0", + " AND (txt LIKE ? OR ct.name LIKE ?)", + " ORDER BY m.timestamp,m.id;" + ) } else { - "SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id \ - LEFT JOIN chats c ON m.chat_id=c.id WHERE m.chat_id>9 AND m.hidden=0 \ - AND (c.blocked=0 OR c.blocked=?) \ - AND ct.blocked=0 AND (m.txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp DESC,m.id DESC;" + concat!( + "SELECT m.id AS id, m.timestamp AS timestamp", + " FROM msgs m", + " LEFT JOIN contacts ct", + " ON m.from_id=ct.id", + " LEFT JOIN chats c", + " ON m.chat_id=c.id", + " WHERE m.chat_id>9", + " AND m.hidden=0", + " AND (c.blocked=0 OR c.blocked=?)", + " AND ct.blocked=0", + " AND (m.txt LIKE ? OR ct.name LIKE ?)", + " ORDER BY m.timestamp DESC,m.id DESC;" + ) }; self.sql .query_map( query, params![chat_id as i32, &strLikeInText, &strLikeBeg], - |row| row.get::<_, i32>(0), + |row| row.get::<_, MsgId>("id"), |rows| { let mut ret = Vec::new(); for id in rows { - ret.push(id? as u32); + ret.push(id?); } Ok(ret) }, @@ -397,7 +421,7 @@ impl Context { } } - pub fn do_heuristics_moves(&self, folder: &str, msg_id: u32) { + pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) { if !self.get_config_bool(Config::MvboxMove) { return; } @@ -422,7 +446,7 @@ impl Context { job_add( self, Action::MoveMsg, - msg.id as libc::c_int, + msg.id.to_u32() as i32, Params::new(), 0, ); diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 31e364ec6..ac0b7a061 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -25,7 +25,7 @@ use crate::error::Result; use crate::events::Event; use crate::job::*; use crate::location; -use crate::message::{self, MessageState}; +use crate::message::{self, MessageState, MsgId}; use crate::param::*; use crate::peerstate::*; use crate::securejoin::handle_securejoin_handshake; @@ -85,7 +85,7 @@ pub unsafe fn dc_receive_imf( let mut hidden = 0; let mut needs_delete_job = false; - let mut insert_msg_id = 0; + let mut insert_msg_id = MsgId::new_unset(); let mut sent_timestamp = 0; let mut created_db_entries = Vec::new(); @@ -97,17 +97,17 @@ pub unsafe fn dc_receive_imf( // helper method to handle early exit and memory cleanup let cleanup = |context: &Context, create_event_to_send: &Option, - created_db_entries: &Vec<(usize, usize)>, - rr_event_to_send: &Vec<(u32, u32)>| { + created_db_entries: &Vec<(usize, MsgId)>, + rr_event_to_send: &Vec<(u32, MsgId)>| { if let Some(create_event_to_send) = create_event_to_send { for (chat_id, msg_id) in created_db_entries { let event = match create_event_to_send { CreateEvent::MsgsChanged => Event::MsgsChanged { - msg_id: *msg_id as u32, + msg_id: *msg_id, chat_id: *chat_id as u32, }, CreateEvent::IncomingMsg => Event::IncomingMsg { - msg_id: *msg_id as u32, + msg_id: *msg_id, chat_id: *chat_id as u32, }, }; @@ -273,7 +273,7 @@ pub unsafe fn dc_receive_imf( job_add( context, Action::DeleteMsgOnImap, - created_db_entries[0].1 as i32, + created_db_entries[0].1.to_u32() as i32, Params::new(), 0, ); @@ -313,8 +313,8 @@ unsafe fn add_parts( flags: u32, needs_delete_job: &mut bool, to_self: i32, - insert_msg_id: &mut u32, - created_db_entries: &mut Vec<(usize, usize)>, + insert_msg_id: &mut MsgId, + created_db_entries: &mut Vec<(usize, MsgId)>, create_event_to_send: &mut Option, ) -> Result<()> { let mut state: MessageState; @@ -684,9 +684,10 @@ unsafe fn add_parts( ])?; txt_raw = None; - *insert_msg_id = + let row_id = sql::get_rowid_with_conn(context, conn, "msgs", "rfc724_mid", &rfc724_mid); - created_db_entries.push((*chat_id as usize, *insert_msg_id as usize)); + *insert_msg_id = MsgId::new(row_id); + created_db_entries.push((*chat_id as usize, *insert_msg_id)); } Ok(()) }, @@ -719,7 +720,7 @@ unsafe fn handle_reports( mime_parser: &MimeParser, from_id: u32, sent_timestamp: i64, - rr_event_to_send: &mut Vec<(u32, u32)>, + rr_event_to_send: &mut Vec<(u32, MsgId)>, server_folder: impl AsRef, server_uid: u32, ) { @@ -810,20 +811,15 @@ unsafe fn handle_reports( if let Ok(rfc724_mid) = wrapmime::parse_message_id(as_str( (*of_org_msgid).fld_value, )) { - let mut chat_id_0 = 0; - let mut msg_id = 0; - - if message::mdn_from_ext( + if let Some((chat_id, msg_id)) = message::mdn_from_ext( context, from_id, &rfc724_mid, sent_timestamp, - &mut chat_id_0, - &mut msg_id, ) { - rr_event_to_send.push((chat_id_0, msg_id)); + rr_event_to_send.push((chat_id, msg_id)); + mdn_consumed = 1; } - mdn_consumed = (msg_id != 0) as libc::c_int; } } } @@ -851,7 +847,7 @@ fn save_locations( mime_parser: &MimeParser, chat_id: u32, from_id: u32, - insert_msg_id: u32, + insert_msg_id: MsgId, hidden: i32, ) { let mut location_id_written = false; diff --git a/src/error.rs b/src/error.rs index 7b340e88d..14c84df85 100644 --- a/src/error.rs +++ b/src/error.rs @@ -32,6 +32,8 @@ pub enum Error { FromUtf8(std::string::FromUtf8Error), #[fail(display = "{}", _0)] BlobError(#[cause] crate::blob::BlobError), + #[fail(display = "Invalid Message ID.")] + InvalidMsgId, } pub type Result = std::result::Result; @@ -102,6 +104,12 @@ impl From for Error { } } +impl From for Error { + fn from(_err: crate::message::InvalidMsgId) -> Error { + Error::InvalidMsgId + } +} + #[macro_export] macro_rules! bail { ($e:expr) => { diff --git a/src/events.rs b/src/events.rs index d5c1f73f8..f74b886ef 100644 --- a/src/events.rs +++ b/src/events.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use strum::EnumProperty; +use crate::message::MsgId; + impl Event { /// Returns the corresponding Event id. pub fn as_id(&self) -> i32 { @@ -125,7 +127,7 @@ pub enum Event { /// /// @return 0 #[strum(props(id = "2000"))] - MsgsChanged { chat_id: u32, msg_id: u32 }, + MsgsChanged { chat_id: u32, msg_id: MsgId }, /// There is a fresh message. Typically, the user will show an notification /// when receiving this message. @@ -134,28 +136,28 @@ pub enum Event { /// /// @return 0 #[strum(props(id = "2005"))] - IncomingMsg { chat_id: u32, msg_id: u32 }, + IncomingMsg { chat_id: u32, msg_id: MsgId }, /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to /// DC_STATE_OUT_DELIVERED, see dc_msg_get_state(). /// /// @return 0 #[strum(props(id = "2010"))] - MsgDelivered { chat_id: u32, msg_id: u32 }, + MsgDelivered { chat_id: u32, msg_id: MsgId }, /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to /// DC_STATE_OUT_FAILED, see dc_msg_get_state(). /// /// @return 0 #[strum(props(id = "2012"))] - MsgFailed { chat_id: u32, msg_id: u32 }, + MsgFailed { chat_id: u32, msg_id: MsgId }, /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to /// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state(). /// /// @return 0 #[strum(props(id = "2015"))] - MsgRead { chat_id: u32, msg_id: u32 }, + MsgRead { chat_id: u32, msg_id: MsgId }, /// Chat changed. The name or the image of a chat group was changed or members were added or removed. /// Or the verify state of a chat has changed. diff --git a/src/imap.rs b/src/imap.rs index 828fbedec..d656b9e6b 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1540,7 +1540,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server job_add( context, Action::MarkseenMsgOnImap, - msg_id as libc::c_int, + msg_id.to_u32() as i32, Params::new(), 0, ); diff --git a/src/imex.rs b/src/imex.rs index c576a33b1..36631227c 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -17,7 +17,7 @@ use crate::error::*; use crate::events::Event; use crate::job::*; use crate::key::*; -use crate::message::Message; +use crate::message::{Message, MsgId}; use crate::param::*; use crate::pgp; use crate::sql::{self, Sql}; @@ -228,8 +228,8 @@ pub fn create_setup_code(_context: &Context) -> String { ret } -pub fn continue_key_transfer(context: &Context, msg_id: u32, setup_code: &str) -> Result<()> { - ensure!(msg_id > DC_MSG_ID_LAST_SPECIAL, "wrong id"); +pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) -> Result<()> { + ensure!(!msg_id.is_special(), "wrong id"); let msg = Message::load_from_db(context, msg_id); if msg.is_err() { diff --git a/src/job.rs b/src/job.rs index 7f60763dd..34d614f60 100644 --- a/src/job.rs +++ b/src/job.rs @@ -16,6 +16,7 @@ use crate::imap::*; use crate::imex::*; use crate::location; use crate::login_param::LoginParam; +use crate::message::MsgId; use crate::message::{self, Message, MessageState}; use crate::mimefactory::{vec_contains_lowercase, Loaded, MimeFactory}; use crate::param::*; @@ -156,7 +157,9 @@ impl Job { /* if there is a msg-id and it does not exist in the db, cancel sending. this happends if dc_delete_msgs() was called before the generated mime was sent out */ - if 0 != self.foreign_id && !message::exists(context, self.foreign_id) { + if 0 != self.foreign_id + && !message::exists(context, MsgId::new(self.foreign_id)) + { warn!( context, "Not sending Message {} as it was deleted", self.foreign_id @@ -180,7 +183,7 @@ impl Job { if 0 != self.foreign_id { message::update_msg_state( context, - self.foreign_id, + MsgId::new(self.foreign_id), MessageState::OutDelivered, ); let chat_id: i32 = context @@ -193,7 +196,7 @@ impl Job { .unwrap_or_default(); context.call_cb(Event::MsgDelivered { chat_id: chat_id as u32, - msg_id: self.foreign_id, + msg_id: MsgId::new(self.foreign_id), }); } // now also delete the generated file @@ -217,7 +220,7 @@ impl Job { fn do_DC_JOB_MOVE_MSG(&mut self, context: &Context) { let inbox = context.inbox.read().unwrap(); - if let Ok(msg) = Message::load_from_db(context, self.foreign_id) { + if let Ok(msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) { if context .sql .get_raw_config_int(context, "folders_configured") @@ -262,7 +265,7 @@ impl Job { fn do_DC_JOB_DELETE_MSG_ON_IMAP(&mut self, context: &Context) { let inbox = context.inbox.read().unwrap(); - if let Ok(mut msg) = Message::load_from_db(context, self.foreign_id) { + if let Ok(mut msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) { if !msg.rfc724_mid.is_empty() { /* eg. device messages have no Message-ID */ if message::rfc724_mid_cnt(context, &msg.rfc724_mid) > 1 { @@ -290,7 +293,7 @@ impl Job { fn do_DC_JOB_MARKSEEN_MSG_ON_IMAP(&mut self, context: &Context) { let inbox = context.inbox.read().unwrap(); - if let Ok(msg) = Message::load_from_db(context, self.foreign_id) { + if let Ok(msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) { let folder = msg.server_folder.as_ref().unwrap(); match inbox.set_seen(context, folder, msg.server_uid) { ImapResult::RetryLater => { @@ -561,7 +564,7 @@ pub fn job_action_exists(context: &Context, action: Action) -> bool { /* special case for DC_JOB_SEND_MSG_TO_SMTP */ #[allow(non_snake_case)] -pub fn job_send_msg(context: &Context, msg_id: u32) -> Result<(), Error> { +pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { let mut mimefactory = MimeFactory::load_msg(context, msg_id)?; if chat::msgtype_has_file(mimefactory.msg.type_0) { @@ -858,7 +861,11 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) { } } else { if job.action == Action::SendMsgToSmtp { - message::set_msg_failed(context, job.foreign_id, job.pending_error.as_ref()); + message::set_msg_failed( + context, + MsgId::new(job.foreign_id), + job.pending_error.as_ref(), + ); } job.delete(context); } @@ -909,7 +916,7 @@ pub fn connect_to_inbox(context: &Context, inbox: &Imap) -> libc::c_int { ret_connected } -fn send_mdn(context: &Context, msg_id: u32) -> Result<(), Error> { +fn send_mdn(context: &Context, msg_id: MsgId) -> Result<(), Error> { let mut mimefactory = MimeFactory::load_mdn(context, msg_id)?; unsafe { mimefactory.render()? }; add_smtp_job(context, Action::SendMdn, &mut mimefactory)?; @@ -938,7 +945,7 @@ fn add_smtp_job(context: &Context, action: Action, mimefactory: &MimeFactory) -> context, action, (if mimefactory.loaded == Loaded::Message { - mimefactory.msg.id + mimefactory.msg.id.to_u32() as i32 } else { 0 }) as libc::c_int, diff --git a/src/location.rs b/src/location.rs index 8429f9d80..f0d0a943d 100644 --- a/src/location.rs +++ b/src/location.rs @@ -11,7 +11,7 @@ use crate::dc_tools::*; use crate::error::Error; use crate::events::Event; use crate::job::*; -use crate::message::Message; +use crate::message::{Message, MsgId}; use crate::param::*; use crate::sql; use crate::stock::StockMessage; @@ -475,12 +475,16 @@ pub fn set_kml_sent_timestamp( Ok(()) } -pub fn set_msg_location_id(context: &Context, msg_id: u32, location_id: u32) -> Result<(), Error> { +pub fn set_msg_location_id( + context: &Context, + msg_id: MsgId, + location_id: u32, +) -> Result<(), Error> { sql::execute( context, &context.sql, "UPDATE msgs SET location_id=? WHERE id=?;", - params![location_id, msg_id as i32], + params![location_id, msg_id], )?; Ok(()) diff --git a/src/message.rs b/src/message.rs index f4c284020..3e2ea253d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use deltachat_derive::{FromSql, ToSql}; +use failure::Fail; use crate::chat::{self, Chat}; use crate::constants::*; @@ -17,9 +18,134 @@ use crate::pgp::*; use crate::sql; use crate::stock::StockMessage; -/// In practice, the user additionally cuts the string himself pixel-accurate. +// In practice, the user additionally cuts the string themselves +// pixel-accurate. const SUMMARY_CHARACTERS: usize = 160; +/// Message ID, including reserved IDs. +/// +/// Some message IDs are reserved to identify special message types. +/// This type can represent both the special as well as normal +/// messages. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct MsgId(u32); + +impl MsgId { + /// Create a new [MsgId]. + pub fn new(id: u32) -> MsgId { + MsgId(id) + } + + /// Create a new unset [MsgId]. + pub fn new_unset() -> MsgId { + MsgId(0) + } + + /// Whether the message ID signifies a special message. + /// + /// This kind of message ID can not be used for real messages. + pub fn is_special(&self) -> bool { + match self.0 { + 0..=DC_MSG_ID_LAST_SPECIAL => true, + _ => false, + } + } + + /// Whether the message ID is unset. + /// + /// When a message is created it initially has a ID of `0`, which + /// is filled in by a real message ID once the message is saved in + /// the database. This returns true while the message has not + /// been saved and thus not yet been given an actual message ID. + /// + /// When this is `true`, [MsgId::is_special] will also always be + /// `true`. + pub fn is_unset(&self) -> bool { + self.0 == 0 + } + + /// Whether the message ID is the special marker1 marker. + /// + /// See the docs of the `dc_get_chat_msgs` C API for details. + pub fn is_marker1(&self) -> bool { + self.0 == DC_MSG_ID_MARKER1 + } + + /// Whether the message ID is the special day marker. + /// + /// See the docs of the `dc_get_chat_msgs` C API for details. + pub fn is_daymarker(&self) -> bool { + self.0 == DC_MSG_ID_DAYMARKER + } + + /// Bad evil escape hatch. + /// + /// Avoid using this, eventually types should be cleaned up enough + /// that it is no longer necessary. + pub fn to_u32(&self) -> u32 { + self.0 + } +} + +impl std::fmt::Display for MsgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Would be nice if we could use match here, but no computed values in ranges. + if self.0 == DC_MSG_ID_MARKER1 { + write!(f, "Msg#Marker1") + } else if self.0 == DC_MSG_ID_DAYMARKER { + write!(f, "Msg#DayMarker") + } else if self.0 <= DC_MSG_ID_LAST_SPECIAL { + write!(f, "Msg#UnknownSpecial") + } else { + write!(f, "Msg#{}", self.0) + } + } +} + +/// Allow converting [MsgId] to an SQLite type. +/// +/// This allows you to directly store [MsgId] into the database. +/// +/// # Errors +/// +/// This **does** ensure that no special message IDs are written into +/// the database and the conversion will fail if this is not the case. +impl rusqlite::types::ToSql for MsgId { + fn to_sql(&self) -> rusqlite::Result { + if self.0 <= DC_MSG_ID_LAST_SPECIAL { + return Err(rusqlite::Error::ToSqlConversionFailure(Box::new( + InvalidMsgId.compat(), + ))); + } + let val = rusqlite::types::Value::Integer(self.0 as i64); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +/// Allow converting an SQLite integer directly into [MsgId]. +impl rusqlite::types::FromSql for MsgId { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + // Would be nice if we could use match here, but alas. + i64::column_result(value).and_then(|val| { + if 0 <= val && val <= std::u32::MAX as i64 { + Ok(MsgId::new(val as u32)) + } else { + Err(rusqlite::types::FromSqlError::OutOfRange(val)) + } + }) + } +} + +/// Message ID was invalid. +/// +/// This usually occurs when trying to use a message ID of +/// [DC_MSG_ID_LAST_SPECIAL] or below in a situation where this is not +/// possible. +#[derive(Debug, Fail)] +#[fail(display = "Invalid Message ID.")] +pub struct InvalidMsgId; + /// An object representing a single message in memory. /// The message object is not updated. /// If you want an update, you have to recreate the object. @@ -29,7 +155,7 @@ const SUMMARY_CHARACTERS: usize = 160; /// approx. max. length returned by dc_get_msg_info() #[derive(Debug, Clone, Default)] pub struct Message { - pub(crate) id: u32, + pub(crate) id: MsgId, pub(crate) from_id: u32, pub(crate) to_id: u32, pub(crate) chat_id: u32, @@ -61,70 +187,105 @@ impl Message { msg } - pub fn load_from_db(context: &Context, id: u32) -> Result { + pub fn load_from_db(context: &Context, id: MsgId) -> Result { + ensure!( + !id.is_special(), + "Can not load special message IDs from DB." + ); context.sql.query_row( - "SELECT \ - m.id,rfc724_mid,m.mime_in_reply_to,m.server_folder,m.server_uid,m.move_state,m.chat_id, \ - m.from_id,m.to_id,m.timestamp,m.timestamp_sent,m.timestamp_rcvd, m.type,m.state,m.msgrmsg,m.txt, \ - m.param,m.starred,m.hidden,m.location_id, c.blocked \ - FROM msgs m \ - LEFT JOIN chats c ON c.id=m.chat_id WHERE m.id=?;", - params![id as i32], - |row| { - let mut msg = Message::default(); - msg.id = row.get::<_, i32>(0)? as u32; - msg.rfc724_mid = row.get::<_, String>(1)?; - msg.in_reply_to = row.get::<_, Option>(2)?; - msg.server_folder = row.get::<_, Option>(3)?; - msg.server_uid = row.get(4)?; - msg.move_state = row.get(5)?; - msg.chat_id = row.get(6)?; - msg.from_id = row.get(7)?; - msg.to_id = row.get(8)?; - msg.timestamp_sort = row.get(9)?; - msg.timestamp_sent = row.get(10)?; - msg.timestamp_rcvd = row.get(11)?; - msg.type_0 = row.get(12)?; - msg.state = row.get(13)?; - msg.is_dc_message = row.get(14)?; + concat!( + "SELECT", + " m.id AS id,", + " rfc724_mid AS rfc724mid,", + " m.mime_in_reply_to AS mime_in_reply_to,", + " m.server_folder AS server_folder,", + " m.server_uid AS server_uid,", + " m.move_state as move_state,", + " m.chat_id AS chat_id,", + " m.from_id AS from_id,", + " m.to_id AS to_id,", + " m.timestamp AS timestamp,", + " m.timestamp_sent AS timestamp_sent,", + " m.timestamp_rcvd AS timestamp_rcvd,", + " m.type AS type,", + " m.state AS state,", + " m.msgrmsg AS msgrmsg,", + " m.txt AS txt,", + " m.param AS param,", + " m.starred AS starred,", + " m.hidden AS hidden,", + " m.location_id AS location,", + " c.blocked AS blocked", + " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", + " WHERE m.id=?;" + ), + params![id], + |row| { + let mut msg = Message::default(); + // msg.id = row.get::<_, AnyMsgId>("id")?; + msg.id = row.get("id")?; + msg.rfc724_mid = row.get::<_, String>("rfc724mid")?; + msg.in_reply_to = row.get::<_, Option>("mime_in_reply_to")?; + msg.server_folder = row.get::<_, Option>("server_folder")?; + msg.server_uid = row.get("server_uid")?; + msg.move_state = row.get("move_state")?; + msg.chat_id = row.get("chat_id")?; + msg.from_id = row.get("from_id")?; + msg.to_id = row.get("to_id")?; + msg.timestamp_sort = row.get("timestamp")?; + msg.timestamp_sent = row.get("timestamp_sent")?; + msg.timestamp_rcvd = row.get("timestamp_rcvd")?; + msg.type_0 = row.get("type")?; + msg.state = row.get("state")?; + msg.is_dc_message = row.get("msgrmsg")?; - let text; - if let rusqlite::types::ValueRef::Text(buf) = row.get_raw(15) { - if let Ok(t) = String::from_utf8(buf.to_vec()) { - text = t; + let text; + if let rusqlite::types::ValueRef::Text(buf) = row.get_raw("txt") { + if let Ok(t) = String::from_utf8(buf.to_vec()) { + text = t; + } else { + warn!( + context, + concat!( + "dc_msg_load_from_db: could not get ", + "text column as non-lossy utf8 id {}" + ), + id + ); + text = String::from_utf8_lossy(buf).into_owned(); + } } else { - warn!(context, "dc_msg_load_from_db: could not get text column as non-lossy utf8 id {}", id); - text = String::from_utf8_lossy(buf).into_owned(); + text = "".to_string(); } - } else { - text = "".to_string(); - } - msg.text = Some(text); + msg.text = Some(text); - msg.param = row.get::<_, String>(16)?.parse().unwrap_or_default(); - msg.starred = row.get(17)?; - msg.hidden = row.get(18)?; - msg.location_id = row.get(19)?; - msg.chat_blocked = row.get::<_, Option>(20)?.unwrap_or_default(); + msg.param = row.get::<_, String>("param")?.parse().unwrap_or_default(); + msg.starred = row.get("starred")?; + msg.hidden = row.get("hidden")?; + msg.location_id = row.get("location")?; + msg.chat_blocked = row + .get::<_, Option>("blocked")? + .unwrap_or_default(); - Ok(msg) - }) + Ok(msg) + }, + ) } - pub fn delete_from_db(context: &Context, msg_id: u32) { + pub fn delete_from_db(context: &Context, msg_id: MsgId) { if let Ok(msg) = Message::load_from_db(context, msg_id) { sql::execute( context, &context.sql, "DELETE FROM msgs WHERE id=?;", - params![msg.id as i32], + params![msg.id], ) .ok(); sql::execute( context, &context.sql, "DELETE FROM msgs_mdns WHERE msg_id=?;", - params![msg.id as i32], + params![msg.id], ) .ok(); } @@ -188,7 +349,7 @@ impl Message { } } - pub fn get_id(&self) -> u32 { + pub fn get_id(&self) -> MsgId { self.id } @@ -400,7 +561,7 @@ impl Message { context, &context.sql, "UPDATE msgs SET param=? WHERE id=?;", - params![self.param.to_string(), self.id as i32], + params![self.param.to_string(), self.id], ) .is_ok() } @@ -510,7 +671,7 @@ impl Lot { } } -pub fn get_msg_info(context: &Context, msg_id: u32) -> String { +pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String { let mut ret = String::new(); let msg = Message::load_from_db(context, msg_id); @@ -523,11 +684,11 @@ pub fn get_msg_info(context: &Context, msg_id: u32) -> String { let rawtxt: Option = context.sql.query_get_value( context, "SELECT txt_raw FROM msgs WHERE id=?;", - params![msg_id as i32], + params![msg_id], ); if rawtxt.is_none() { - ret += &format!("Cannot load message #{}.", msg_id as usize); + ret += &format!("Cannot load message {}.", msg_id); return ret; } let rawtxt = rawtxt.unwrap_or_default(); @@ -560,7 +721,7 @@ pub fn get_msg_info(context: &Context, msg_id: u32) -> String { if let Ok(rows) = context.sql.query_map( "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;", - params![msg_id as i32], + params![msg_id], |row| { let contact_id: i32 = row.get(0)?; let ts: i64 = row.get(1)?; @@ -670,21 +831,21 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { Some(info) } -pub fn get_mime_headers(context: &Context, msg_id: u32) -> Option { +pub fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option { context.sql.query_get_value( context, "SELECT mime_headers FROM msgs WHERE id=?;", - params![msg_id as i32], + params![msg_id], ) } -pub fn delete_msgs(context: &Context, msg_ids: &[u32]) { +pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) { for msg_id in msg_ids.iter() { update_msg_chat_id(context, *msg_id, DC_CHAT_ID_TRASH); job_add( context, Action::DeleteMsgOnImap, - *msg_id as libc::c_int, + msg_id.to_u32() as i32, Params::new(), 0, ); @@ -693,35 +854,45 @@ pub fn delete_msgs(context: &Context, msg_ids: &[u32]) { if !msg_ids.is_empty() { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); job_kill_action(context, Action::Housekeeping); job_add(context, Action::Housekeeping, 0, Params::new(), 10); }; } -fn update_msg_chat_id(context: &Context, msg_id: u32, chat_id: u32) -> bool { +fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: u32) -> bool { sql::execute( context, &context.sql, "UPDATE msgs SET chat_id=? WHERE id=?;", - params![chat_id as i32, msg_id as i32], + params![chat_id as i32, msg_id], ) .is_ok() } -pub fn markseen_msgs(context: &Context, msg_ids: &[u32]) -> bool { +pub fn markseen_msgs(context: &Context, msg_ids: &[MsgId]) -> bool { if msg_ids.is_empty() { return false; } let msgs = context.sql.prepare( - "SELECT m.state, c.blocked FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id WHERE m.id=? AND m.chat_id>9", + concat!( + "SELECT", + " m.state AS state,", + " c.blocked AS blocked", + " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", + " WHERE m.id=? AND m.chat_id>9" + ), |mut stmt, _| { let mut res = Vec::with_capacity(msg_ids.len()); for id in msg_ids.iter() { - let query_res = stmt.query_row(params![*id as i32], |row| { - Ok((row.get::<_, MessageState>(0)?, row.get::<_, Option>(1)?.unwrap_or_default())) + let query_res = stmt.query_row(params![*id], |row| { + Ok(( + row.get::<_, MessageState>("state")?, + row.get::<_, Option>("blocked")? + .unwrap_or_default(), + )) }); if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res { continue; @@ -731,7 +902,7 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[u32]) -> bool { } Ok(res) - } + }, ); if msgs.is_err() { @@ -745,12 +916,12 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[u32]) -> bool { if curr_blocked == Blocked::Not { if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed { update_msg_state(context, *id, MessageState::InSeen); - info!(context, "Seen message #{}.", id); + info!(context, "Seen message {}.", id); job_add( context, Action::MarkseenMsgOnImap, - *id as i32, + id.to_u32() as i32, Params::new(), 0, ); @@ -765,24 +936,24 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[u32]) -> bool { if send_event { context.call_cb(Event::MsgsChanged { chat_id: 0, - msg_id: 0, + msg_id: MsgId::new(0), }); } true } -pub fn update_msg_state(context: &Context, msg_id: u32, state: MessageState) -> bool { +pub fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageState) -> bool { sql::execute( context, &context.sql, "UPDATE msgs SET state=? WHERE id=?;", - params![state, msg_id as i32], + params![state, msg_id], ) .is_ok() } -pub fn star_msgs(context: &Context, msg_ids: &[u32], star: bool) -> bool { +pub fn star_msgs(context: &Context, msg_ids: &[MsgId], star: bool) -> bool { if msg_ids.is_empty() { return false; } @@ -790,7 +961,7 @@ pub fn star_msgs(context: &Context, msg_ids: &[u32], star: bool) -> bool { .sql .prepare("UPDATE msgs SET starred=? WHERE id=?;", |mut stmt, _| { for msg_id in msg_ids.iter() { - stmt.execute(params![star as i32, *msg_id as i32])?; + stmt.execute(params![star as i32, *msg_id])?; } Ok(()) }) @@ -870,8 +1041,8 @@ pub fn get_summarytext_by_raw( // Context functions to work with messages -pub fn exists(context: &Context, msg_id: u32) -> bool { - if msg_id <= DC_CHAT_ID_LAST_SPECIAL { +pub fn exists(context: &Context, msg_id: MsgId) -> bool { + if msg_id.is_special() { return false; } @@ -900,7 +1071,7 @@ pub fn update_msg_move_state(context: &Context, rfc724_mid: &str, state: MoveSta .is_ok() } -pub fn set_msg_failed(context: &Context, msg_id: u32, error: Option>) { +pub fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option>) { if let Ok(mut msg) = Message::load_from_db(context, msg_id) { if msg.state.can_fail() { msg.state = MessageState::OutFailed; @@ -914,7 +1085,7 @@ pub fn set_msg_failed(context: &Context, msg_id: u32, error: Option bool { - if from_id <= 9 || rfc724_mid.is_empty() || *ret_chat_id != 0 || *ret_msg_id != 0 { - return false; +) -> Option<(u32, MsgId)> { + if from_id <= 9 || rfc724_mid.is_empty() { + return None; } - let mut read_by_all = false; - if let Ok((msg_id, chat_id, chat_type, msg_state)) = context.sql.query_row( - "SELECT m.id, c.id, c.type, m.state FROM msgs m \ - LEFT JOIN chats c ON m.chat_id=c.id \ - WHERE rfc724_mid=? AND from_id=1 \ - ORDER BY m.id;", + concat!( + "SELECT", + " m.id AS msg_id,", + " c.id AS chat_id,", + " c.type AS type,", + " m.state AS state", + " FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id", + " WHERE rfc724_mid=? AND from_id=1", + " ORDER BY m.id;" + ), params![rfc724_mid], |row| { Ok(( - row.get::<_, i32>(0)?, - row.get::<_, i32>(1)?, - row.get::<_, Chattype>(2)?, - row.get::<_, MessageState>(3)?, + row.get::<_, MsgId>("msg_id")?, + row.get::<_, u32>("chat_id")?, + row.get::<_, Chattype>("type")?, + row.get::<_, MessageState>("state")?, )) }, ) { - *ret_msg_id = msg_id as u32; - *ret_chat_id = chat_id as u32; + let mut read_by_all = false; // if already marked as MDNS_RCVD msgstate_can_fail() returns false. // however, it is important, that ret_msg_id is set above as this @@ -967,20 +1139,20 @@ pub fn mdn_from_ext( .sql .exists( "SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;", - params![*ret_msg_id as i32, from_id as i32,], + params![msg_id, from_id as i32,], ) .unwrap_or_default(); if !mdn_already_in_table { context.sql.execute( "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);", - params![*ret_msg_id as i32, from_id as i32, timestamp_sent], + params![msg_id, from_id as i32, timestamp_sent], ).unwrap_or_default(); // TODO: better error handling } // Normal chat? that's quite easy. if chat_type == Chattype::Single { - update_msg_state(context, *ret_msg_id, MessageState::OutMdnRcvd); + update_msg_state(context, msg_id, MessageState::OutMdnRcvd); read_by_all = true; } else { // send event about new state @@ -989,7 +1161,7 @@ pub fn mdn_from_ext( .query_get_value::<_, isize>( context, "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;", - params![*ret_msg_id as i32], + params![msg_id], ) .unwrap_or_default() as usize; /* @@ -1005,16 +1177,19 @@ pub fn mdn_from_ext( (S=Sender, R=Recipient) */ // for rounding, SELF is already included! - let soll_cnt = (chat::get_chat_contact_cnt(context, *ret_chat_id) + 1) / 2; + let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id) + 1) / 2; if ist_cnt >= soll_cnt { - update_msg_state(context, *ret_msg_id, MessageState::OutMdnRcvd); + update_msg_state(context, msg_id, MessageState::OutMdnRcvd); read_by_all = true; } // else wait for more receipts } } + return match read_by_all { + true => Some((chat_id, msg_id)), + false => None, + }; } - - read_by_all + None } /// The number of messages assigned to real chat (!=deaddrop, !=trash) @@ -1068,7 +1243,7 @@ pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> libc::c_int { pub(crate) fn rfc724_mid_exists( context: &Context, rfc724_mid: &str, -) -> Result<(String, u32, u32), Error> { +) -> Result<(String, u32, MsgId), Error> { ensure!(!rfc724_mid.is_empty(), "empty rfc724_mid"); context.sql.query_row( @@ -1077,7 +1252,7 @@ pub(crate) fn rfc724_mid_exists( |row| { let server_folder = row.get::<_, Option>(0)?.unwrap_or_default(); let server_uid = row.get(1)?; - let msg_id = row.get(2)?; + let msg_id: MsgId = row.get(2)?; Ok((server_folder, server_uid, msg_id)) }, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 898ce84e0..49e0d4280 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -22,6 +22,7 @@ use crate::dc_tools::*; use crate::e2ee::*; use crate::error::Error; use crate::location; +use crate::message::MsgId; use crate::message::{self, Message}; use crate::param::*; use crate::stock::StockMessage; @@ -107,7 +108,7 @@ impl<'a> MimeFactory<'a> { Ok(()) } - pub fn load_mdn(context: &'a Context, msg_id: u32) -> Result { + pub fn load_mdn(context: &'a Context, msg_id: MsgId) -> Result { if !context.get_config_bool(Config::MdnsEnabled) { // MDNs not enabled - check this is late, in the job. the // user may have changed its choice while offline ... @@ -653,8 +654,8 @@ impl<'a> MimeFactory<'a> { Ok(()) } - pub fn load_msg(context: &Context, msg_id: u32) -> Result { - ensure!(msg_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id"); + pub fn load_msg(context: &Context, msg_id: MsgId) -> Result { + ensure!(!msg_id.is_special(), "Invalid chat id"); let msg = Message::load_from_db(context, msg_id)?; let chat = Chat::load_from_db(context, msg.chat_id)?; @@ -720,7 +721,7 @@ impl<'a> MimeFactory<'a> { } let row = context.sql.query_row( "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", - params![factory.msg.id as i32], + params![factory.msg.id], |row| { let in_reply_to: String = row.get(0)?; let references: String = row.get(1)?; diff --git a/src/param.rs b/src/param.rs index 058cc937a..9fcdf0db3 100644 --- a/src/param.rs +++ b/src/param.rs @@ -49,6 +49,14 @@ pub enum Param { /// For Messages Error = b'L', /// For Messages: space-separated list of messaged IDs of forwarded copies. + /// + /// This is used when a [Message] is in the + /// [MessageState::OutPending] state but is already forwarded. + /// In this case the forwarded messages are written to the + /// database and their message IDs are added to this parameter of + /// the original message, which is also saved in the database. + /// When the original message is then finally sent this parameter + /// is used to also send all the forwarded messages. PrepForwards = b'P', /// For Jobs SetLatitude = b'l',