diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 91001b744..3b405bb39 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1096,9 +1096,9 @@ void dc_set_draft (dc_context_t* context, uint32_t ch * Add a message to the device-chat. * Device-messages usually contain update information * and some hints that are added during the program runs, multi-device etc. - * - * Device-messages may be added from the core, - * however, with this function, this can be done from the ui as well. + * The device-message may be defined by a label; + * if a message with the same label was added or skipped before, + * the message is not added again, even if the message was deleted in between. * If needed, the device-chat is created before. * * Sends the event #DC_EVENT_MSGS_CHANGED on success. @@ -1106,33 +1106,50 @@ void dc_set_draft (dc_context_t* context, uint32_t ch * * @memberof dc_context_t * @param context The context as created by dc_context_new(). - * @param msg Message to be added to the device-chat. - * The message appears to the user as an incoming message. - * @return The ID of the added message. - */ -uint32_t dc_add_device_msg (dc_context_t* context, dc_msg_t* msg); - - -/** - * Add a message only one time to the device-chat. - * The device-message is defined by a name. - * If a message with the same name was added before, - * the message is not added again. - * Use dc_add_device_msg() to add device-messages unconditionally. - * - * Sends the event #DC_EVENT_MSGS_CHANGED on success. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). * @param label A unique name for the message to add. * The label is typically not displayed to the user and * must be created from the characters `A-Z`, `a-z`, `0-9`, `_` or `-`. + * If you pass NULL here, the message is added unconditionally. * @param msg Message to be added to the device-chat. * The message appears to the user as an incoming message. - * @return The ID of the added message, - * this might be the id of an older message with the same name. + * If you pass NULL here, only the given label will be added + * and block adding messages with that label in the future. + * @return The ID of the just added message, + * if the message was already added or no message to add is given, 0 is returned. + * + * Example: + * ~~~ + * dc_msg_t* welcome_msg = dc_msg_new(DC_MSG_TEXT); + * dc_msg_set_text(welcome_msg, "great that you give this app a try!"); + * + * dc_msg_t* changelog_msg = dc_msg_new(DC_MSG_TEXT); + * dc_msg_set_text(changelog_msg, "we have added 3 new emojis :)"); + * + * if (dc_add_device_msg(context, "welcome", welcome_msg)) { + * // do not add the changelog on a new installations - + * // not now and not when this code is executed again + * dc_add_device_msg(context, "update-123", NULL); + * } else { + * // welcome message was not added now, this is an oder installation, + * // add a changelog + * dc_add_device_msg(context, "update-123", changelog_msg); + * } + * ~~~ */ -uint32_t dc_add_device_msg_once (dc_context_t* context, const char* label, dc_msg_t* msg); +uint32_t dc_add_device_msg (dc_context_t* context, const char* label, dc_msg_t* msg); + + +/** + * Check if a device-message with a given label was ever added. + * Device-messages can be added dc_add_device_msg(). + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param label Label of the message to check. + * @return 1=A message with this label was added at some point, + * 0=A message with this label was never added. + */ +int dc_was_device_msg_ever_added (dc_context_t* context, const char* label); /** @@ -2760,9 +2777,7 @@ int dc_chat_is_self_talk (const dc_chat_t* chat); * From the ui view, device-talks are not very special, * the user can delete and forward messages, archive the chat, set notifications etc. * - * Messages may be added from the core to the device chat, - * so the chat just pops up as usual. - * However, if needed the ui can also add messages using dc_add_device_msg() + * Messages can be added to the device-talk using dc_add_device_msg() * * @memberof dc_chat_t * @param chat The chat object. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index b7895549d..c5362761a 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -812,40 +812,50 @@ pub unsafe extern "C" fn dc_set_draft( } #[no_mangle] -pub unsafe extern "C" fn dc_add_device_msg(context: *mut dc_context_t, msg: *mut dc_msg_t) -> u32 { - if context.is_null() || msg.is_null() { +pub unsafe extern "C" fn dc_add_device_msg( + context: *mut dc_context_t, + label: *const libc::c_char, + msg: *mut dc_msg_t, +) -> u32 { + if context.is_null() || (label.is_null() && msg.is_null()) { eprintln!("ignoring careless call to dc_add_device_msg()"); return 0; } let ffi_context = &mut *context; - let ffi_msg = &mut *msg; + let msg = if msg.is_null() { + None + } else { + let ffi_msg: &mut MessageWrapper = &mut *msg; + Some(&mut ffi_msg.message) + }; ffi_context .with_inner(|ctx| { - chat::add_device_msg(ctx, &mut ffi_msg.message) - .unwrap_or_log_default(ctx, "Failed to add device message") + chat::add_device_msg( + ctx, + to_opt_string_lossy(label).as_ref().map(|x| x.as_str()), + msg, + ) + .unwrap_or_log_default(ctx, "Failed to add device message") }) .map(|msg_id| msg_id.to_u32()) .unwrap_or(0) } #[no_mangle] -pub unsafe extern "C" fn dc_add_device_msg_once( +pub unsafe extern "C" fn dc_was_device_msg_ever_added( context: *mut dc_context_t, label: *const libc::c_char, - msg: *mut dc_msg_t, -) -> u32 { - if context.is_null() || label.is_null() || msg.is_null() { - eprintln!("ignoring careless call to dc_add_device_msg_once()"); +) -> libc::c_int { + if context.is_null() || label.is_null() { + eprintln!("ignoring careless call to dc_was_device_msg_ever_added()"); return 0; } let ffi_context = &mut *context; - let ffi_msg = &mut *msg; ffi_context .with_inner(|ctx| { - chat::add_device_msg_once(ctx, &to_string_lossy(label), &mut ffi_msg.message) - .unwrap_or_log_default(ctx, "Failed to add device message once") + chat::was_device_msg_ever_added(ctx, &to_string_lossy(label)).unwrap_or(false) + as libc::c_int }) - .map(|msg_id| msg_id.to_u32()) .unwrap_or(0) } diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 365853457..4656b1dd3 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -837,7 +837,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E ); let mut msg = Message::new(Viewtype::Text); msg.set_text(Some(arg1.to_string())); - chat::add_device_msg(context, &mut msg)?; + chat::add_device_msg(context, None, Some(&mut msg))?; } "listmedia" => { ensure!(sel_chat.is_some(), "No chat selected."); diff --git a/src/chat.rs b/src/chat.rs index b722b26c5..8953c82ea 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1951,78 +1951,78 @@ pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef) -> (u32, .unwrap_or((0, false, Blocked::Not)) } -pub fn add_device_msg(context: &Context, msg: &mut Message) -> Result { - add_device_msg_maybe_labelled(context, None, msg) -} - -pub fn add_device_msg_once( - context: &Context, - label: &str, - msg: &mut Message, -) -> Result { - add_device_msg_maybe_labelled(context, Some(label), msg) -} - -fn add_device_msg_maybe_labelled( +pub fn add_device_msg( context: &Context, label: Option<&str>, - msg: &mut Message, + msg: Option<&mut Message>, ) -> Result { + ensure!( + label.is_some() || msg.is_some(), + "device-messages need label, msg or both" + ); let (chat_id, _blocked) = create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not)?; - let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); + let mut msg_id = MsgId::new_unset(); - // chat_id has an sql-index so it makes sense to add this although redundant if let Some(label) = label { - if let Ok(msg_id) = context.sql.query_row( - "SELECT id FROM msgs WHERE chat_id=? AND label=?", - params![chat_id, label], - |row| { - let msg_id: MsgId = row.get(0)?; - Ok(msg_id) - }, - ) { - info!( - context, - "device-message {} already exist as {}", label, msg_id - ); + if was_device_msg_ever_added(context, label)? { + info!(context, "device-message {} already added", label); return Ok(msg_id); } } - prepare_msg_blob(context, msg)?; - unarchive(context, chat_id)?; + if let Some(msg) = msg { + let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); + prepare_msg_blob(context, msg)?; + unarchive(context, chat_id)?; - context.sql.execute( - "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid,label) \ - VALUES (?,?,?, ?,?,?, ?,?,?,?);", - params![ - chat_id, - DC_CONTACT_ID_DEVICE, - DC_CONTACT_ID_SELF, - dc_create_smeared_timestamp(context), - msg.type_0, - MessageState::InFresh, - msg.text.as_ref().map_or("", String::as_str), - msg.param.to_string(), - rfc724_mid, - label.unwrap_or_default(), - ], - )?; + context.sql.execute( + "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \ + VALUES (?,?,?, ?,?,?, ?,?,?);", + params![ + chat_id, + DC_CONTACT_ID_DEVICE, + DC_CONTACT_ID_SELF, + dc_create_smeared_timestamp(context), + msg.type_0, + MessageState::InFresh, + msg.text.as_ref().map_or("", String::as_str), + msg.param.to_string(), + rfc724_mid, + ], + )?; - let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); - let msg_id = MsgId::new(row_id); - context.call_cb(Event::IncomingMsg { chat_id, msg_id }); - info!( - context, - "device-message {} added as {}", - label.unwrap_or("without label"), - msg_id - ); + let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); + msg_id = MsgId::new(row_id); + } + + if let Some(label) = label { + context.sql.execute( + "INSERT INTO devmsglabels (label) VALUES (?);", + params![label], + )?; + } + + if !msg_id.is_unset() { + context.call_cb(Event::IncomingMsg { chat_id, msg_id }); + } Ok(msg_id) } +pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result { + ensure!(!label.is_empty(), "empty label"); + if let Ok(()) = context.sql.query_row( + "SELECT label FROM devmsglabels WHERE label=?", + params![label], + |_| Ok(()), + ) { + return Ok(true); + } + + Ok(false) +} + pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef) { let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); @@ -2131,18 +2131,18 @@ mod tests { } #[test] - fn test_add_device_msg() { + fn test_add_device_msg_unlabelled() { let t = test_context(Some(Box::new(logging_cb))); // add two device-messages let mut msg1 = Message::new(Viewtype::Text); msg1.text = Some("first message".to_string()); - let msg1_id = add_device_msg(&t.ctx, &mut msg1); + let msg1_id = add_device_msg(&t.ctx, None, Some(&mut msg1)); assert!(msg1_id.is_ok()); let mut msg2 = Message::new(Viewtype::Text); msg2.text = Some("second message".to_string()); - let msg2_id = add_device_msg(&t.ctx, &mut msg2); + let msg2_id = add_device_msg(&t.ctx, None, Some(&mut msg2)); assert!(msg2_id.is_ok()); assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); @@ -2166,34 +2166,35 @@ mod tests { } #[test] - fn test_add_device_msg_once() { + fn test_add_device_msg_labelled() { let t = test_context(Some(Box::new(logging_cb))); // add two device-messages with the same label (second attempt is not added) let mut msg1 = Message::new(Viewtype::Text); msg1.text = Some("first message".to_string()); - let msg1_id = add_device_msg_once(&t.ctx, "any-label", &mut msg1); + let msg1_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg1)); assert!(msg1_id.is_ok()); + assert!(!msg1_id.as_ref().unwrap().is_unset()); let mut msg2 = Message::new(Viewtype::Text); msg2.text = Some("second message".to_string()); - let msg2_id = add_device_msg_once(&t.ctx, "any-label", &mut msg2); + let msg2_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2)); assert!(msg2_id.is_ok()); - assert_eq!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); + assert!(msg2_id.as_ref().unwrap().is_unset()); // check added message - let msg2 = message::Message::load_from_db(&t.ctx, msg2_id.unwrap()); - assert!(msg2.is_ok()); - let msg2 = msg2.unwrap(); - assert_eq!(msg1_id.unwrap(), msg2.id); - assert_eq!(msg2.text.as_ref().unwrap(), "first message"); - assert_eq!(msg2.from_id, DC_CONTACT_ID_DEVICE); - assert_eq!(msg2.to_id, DC_CONTACT_ID_SELF); - assert!(!msg2.is_info()); - assert!(!msg2.is_setupmessage()); + let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap()); + assert!(msg1.is_ok()); + let msg1 = msg1.unwrap(); + assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id); + assert_eq!(msg1.text.as_ref().unwrap(), "first message"); + assert_eq!(msg1.from_id, DC_CONTACT_ID_DEVICE); + assert_eq!(msg1.to_id, DC_CONTACT_ID_SELF); + assert!(!msg1.is_info()); + assert!(!msg1.is_setupmessage()); // check device chat - let chat_id = msg2.chat_id; + let chat_id = msg1.chat_id; assert_eq!(get_msg_cnt(&t.ctx, chat_id), 1); assert!(chat_id > DC_CHAT_ID_LAST_SPECIAL); let chat = Chat::load_from_db(&t.ctx, chat_id); @@ -2204,6 +2205,50 @@ mod tests { assert!(!chat.can_send()); assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeviceMessages)); assert!(chat.get_profile_image(&t.ctx).is_some()); + + // delete device message, make sure it is not added again + message::delete_msgs(&t.ctx, &[*msg1_id.as_ref().unwrap()]); + let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap()); + assert!(msg1.is_err() || msg1.unwrap().chat_id == DC_CHAT_ID_TRASH); + let msg3_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2)); + assert!(msg3_id.is_ok()); + assert!(msg2_id.as_ref().unwrap().is_unset()); + } + + #[test] + fn test_add_device_msg_label_only() { + let t = test_context(Some(Box::new(logging_cb))); + let res = add_device_msg(&t.ctx, Some(""), None); + assert!(res.is_err()); + let res = add_device_msg(&t.ctx, Some("some-label"), None); + assert!(res.is_ok()); + + let mut msg = Message::new(Viewtype::Text); + msg.text = Some("message text".to_string()); + + let msg_id = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)); + assert!(msg_id.is_ok()); + assert!(msg_id.as_ref().unwrap().is_unset()); + + let msg_id = add_device_msg(&t.ctx, Some("unused-label"), Some(&mut msg)); + assert!(msg_id.is_ok()); + assert!(!msg_id.as_ref().unwrap().is_unset()); + } + + #[test] + fn test_was_device_msg_ever_added() { + let t = test_context(Some(Box::new(logging_cb))); + add_device_msg(&t.ctx, Some("some-label"), None).ok(); + assert!(was_device_msg_ever_added(&t.ctx, "some-label").unwrap()); + + let mut msg = Message::new(Viewtype::Text); + msg.text = Some("message text".to_string()); + add_device_msg(&t.ctx, Some("another-label"), Some(&mut msg)).ok(); + assert!(was_device_msg_ever_added(&t.ctx, "another-label").unwrap()); + + assert!(!was_device_msg_ever_added(&t.ctx, "unused-label").unwrap()); + + assert!(was_device_msg_ever_added(&t.ctx, "").is_err()); } fn chatlist_len(ctx: &Context, listflags: usize) -> usize { @@ -2218,7 +2263,7 @@ mod tests { let t = dummy_context(); let mut msg = Message::new(Viewtype::Text); msg.text = Some("foo".to_string()); - let msg_id = add_device_msg(&t.ctx, &mut msg).unwrap(); + let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap(); let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id) .unwrap() .chat_id; diff --git a/src/imex.rs b/src/imex.rs index 8248cb3a6..756fe15b2 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -247,7 +247,7 @@ fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { go to the settings and enable \"Send copy to self\"." .to_string(), ); - chat::add_device_msg_once(context, "bcc-self-hint", &mut msg)?; + chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?; } Ok(()) } diff --git a/src/sql.rs b/src/sql.rs index 396b939d3..8f70eea50 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -810,24 +810,23 @@ fn open( )?; sql.set_raw_config_int(context, "dbversion", 55)?; } - if dbversion < 57 { - info!(context, "[migration] v57"); - // label is a unique name and is currently used for device-messages only. - // in contrast to rfc724_mid and other fields, the label is generated on the device - // and allows reliable identifications this way. + if dbversion < 59 { + info!(context, "[migration] v59"); + // records in the devmsglabels are kept when the message is deleted. + // so, msg_id may or may not exist. sql.execute( - "ALTER TABLE msgs ADD COLUMN label TEXT DEFAULT '';", - params![], + "CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);", + NO_PARAMS, + )?; + sql.execute( + "CREATE INDEX devmsglabels_index1 ON devmsglabels (label);", + NO_PARAMS, )?; if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() { sql.set_raw_config_int(context, "bcc_self", 1)?; } - sql.set_raw_config_int(context, "dbversion", 57)?; - } - if dbversion < 58 { - info!(context, "[migration] v58"); update_icons = true; - sql.set_raw_config_int(context, "dbversion", 58)?; + sql.set_raw_config_int(context, "dbversion", 59)?; } // (2) updates that require high-level objects