diff --git a/CHANGELOG.md b/CHANGELOG.md index 925b13aa8..94fbedca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix sending duplicate messages to group for other clients (like Signal Desktop). (#142) - Fix storing of outgoing messages. (#144) +### Changed + +- Handle message deletion sent by contacts. (#147) + ## [0.5.0] ### Added diff --git a/Cargo.toml b/Cargo.toml index 5ae9b6715..e737924df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,8 @@ authors = ["Gabriel FĂ©ron "] edition = "2021" [dependencies] -libsignal-service = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "791c521" } -libsignal-service-hyper = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "791c521" } +libsignal-service = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "975717c74696c75cbecbf124f2df0fb028e3d82e" } +libsignal-service-hyper = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "975717c74696c75cbecbf124f2df0fb028e3d82e" } async-trait = "0.1" base64 = "0.12" diff --git a/examples/cli.rs b/examples/cli.rs index ac84fa32b..787cb45ad 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -230,6 +230,44 @@ async fn process_incoming_message( attachments_tmp_dir: &Path, notifications: bool, content: &Content, +) { + print_message(manager, notifications, content); + + let sender = content.metadata.sender.uuid; + if let ContentBody::DataMessage(DataMessage { attachments, .. }) = &content.body { + for attachment_pointer in attachments { + let Ok(attachment_data) = manager.get_attachment(attachment_pointer).await else { + log::warn!("failed to fetch attachment"); + continue; + }; + + let extensions = mime_guess::get_mime_extensions_str( + attachment_pointer + .content_type + .as_deref() + .unwrap_or("application/octet-stream"), + ); + let extension = extensions.and_then(|e| e.first()).unwrap_or(&"bin"); + let filename = attachment_pointer + .file_name + .clone() + .unwrap_or_else(|| Local::now().format("%Y-%m-%d-%H-%M-%s").to_string()); + let file_path = attachments_tmp_dir.join(format!("presage-{filename}.{extension}",)); + match fs::write(&file_path, &attachment_data).await { + Ok(_) => info!("saved attachment from {sender} to {}", file_path.display()), + Err(error) => error!( + "failed to write attachment from {sender} to {}: {error}", + file_path.display() + ), + } + } + } +} + +fn print_message( + manager: &Manager, + notifications: bool, + content: &Content, ) { let Ok(thread) = Thread::try_from(content) else { log::warn!("failed to derive thread from content"); @@ -270,7 +308,7 @@ async fn process_incoming_message( DataMessage { body: Some(body), .. } => Some(body.to_string()), - _ => None, + _ => Some("Empty data message".to_string()), }; let format_contact = |uuid| { @@ -297,8 +335,11 @@ async fn process_incoming_message( Sent(&'a Thread, String), } - let sender = content.metadata.sender.uuid; if let Some(msg) = match &content.body { + ContentBody::NullMessage(_) => Some(Msg::Received( + &thread, + "Null message (for example deleted)".to_string(), + )), ContentBody::DataMessage(data_message) => { format_data_message(&thread, data_message).map(|body| Msg::Received(&thread, body)) } @@ -317,26 +358,28 @@ async fn process_incoming_message( None } } { + let ts = content.metadata.timestamp; let (prefix, body) = match msg { Msg::Received(Thread::Contact(sender), body) => { let contact = format_contact(sender); - (format!("Received from {contact}"), body) + (format!("From {contact} @ {ts}: "), body) } Msg::Sent(Thread::Contact(recipient), body) => { let contact = format_contact(recipient); - (format!("Sent to {contact}"), body) + (format!("To {contact} @ {ts}"), body) } Msg::Received(Thread::Group(key), body) => { + let sender = content.metadata.sender.uuid; let group = format_group(key); - (format!("Received from {sender} in group {group}"), body) + (format!("From {sender} to group {group} @ {ts}: "), body) } Msg::Sent(Thread::Group(key), body) => { let group = format_group(key); - (format!("Sent in group: {group}"), body) + (format!("To group {group} @ {ts}"), body) } }; - println!("{prefix} {body}"); + println!("{prefix} / {body}"); if notifications { if let Err(e) = Notification::new() @@ -349,35 +392,6 @@ async fn process_incoming_message( } } } - - if let ContentBody::DataMessage(DataMessage { attachments, .. }) = &content.body { - for attachment_pointer in attachments { - let Ok(attachment_data) = manager.get_attachment(attachment_pointer).await else { - log::warn!("failed to fetch attachment"); - continue; - }; - - let extensions = mime_guess::get_mime_extensions_str( - attachment_pointer - .content_type - .as_deref() - .unwrap_or("application/octet-stream"), - ); - let extension = extensions.and_then(|e| e.first()).unwrap_or(&"bin"); - let filename = attachment_pointer - .file_name - .clone() - .unwrap_or_else(|| Local::now().format("%Y-%m-%d-%H-%M-%s").to_string()); - let file_path = attachments_tmp_dir.join(format!("presage-{filename}.{extension}",)); - match fs::write(&file_path, &attachment_data).await { - Ok(_) => info!("saved attachment from {sender} to {}", file_path.display()), - Err(error) => error!( - "failed to write attachment from {sender} to {}: {error}", - file_path.display() - ), - } - } - } } async fn receive( @@ -536,7 +550,7 @@ async fn run(subcommand: Cmd, config_store: C) -> anyho for group in manager.groups()? { match group { Ok(( - _, + group_master_key, Group { title, description, @@ -545,8 +559,9 @@ async fn run(subcommand: Cmd, config_store: C) -> anyho .. }, )) => { + let key = hex::encode(group_master_key); println!( - "{title}: {description:?} / revision {revision} / {} members", + "{key} {title}: {description:?} / revision {revision} / {} members", members.len() ); } @@ -605,14 +620,17 @@ async fn run(subcommand: Cmd, config_store: C) -> anyho recipient_uuid, from, } => { + let manager = Manager::load_registered(config_store)?; let thread = match (group_master_key, recipient_uuid) { (Some(master_key), _) => Thread::Group(master_key), (_, Some(uuid)) => Thread::Contact(uuid), _ => unreachable!(), }; - let iter = config_store.messages(&thread, from.unwrap_or(0)..)?; - for msg in iter.filter_map(Result::ok) { - println!("{:?}: {:?}", msg.metadata.sender, msg); + for msg in manager + .messages(&thread, from.unwrap_or(0)..)? + .filter_map(Result::ok) + { + print_message(&manager, false, &msg); } } } diff --git a/src/manager.rs b/src/manager.rs index 81dd9a40c..db44d977c 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -25,7 +25,7 @@ use libsignal_service::{ protocol::{KeyPair, PrivateKey, PublicKey}, Content, Envelope, ProfileKey, PushService, Uuid, }, - proto::{sync_message, AttachmentPointer, GroupContextV2}, + proto::{data_message::Delete, sync_message, AttachmentPointer, GroupContextV2, NullMessage}, provisioning::{ generate_registration_id, LinkingManager, ProvisioningManager, SecondaryDeviceProvisioning, VerificationCodeResponse, @@ -1126,15 +1126,36 @@ fn save_message_with_thread( thread: Thread, ) -> Result<(), Error> { // only save DataMessage and SynchronizeMessage (sent) - match message.body { - ContentBody::DataMessage(_) - | ContentBody::SynchronizeMessage(SyncMessage { sent: Some(_), .. }) + match &message.body { + ContentBody::NullMessage(_) => config_store.save_message(&thread, message)?, + ContentBody::DataMessage(d) | ContentBody::SynchronizeMessage(SyncMessage { + sent: Some(sync_message::Sent { + message: Some(d), .. + }), + .. + }) => match d { + DataMessage { + delete: + Some(Delete { + target_sent_timestamp: Some(ts), + }), + .. + } => { + // replace an existing message by an empty NullMessage + if let Some(mut existing_msg) = config_store.message(&thread, *ts)? { + existing_msg.metadata.sender.uuid = Uuid::nil(); + existing_msg.body = NullMessage::default().into(); + config_store.save_message(&thread, existing_msg)?; + debug!("message in thread {thread} @ {ts} deleted"); + } + } + _ => config_store.save_message(&thread, message)?, + }, + ContentBody::SynchronizeMessage(SyncMessage { call_event: Some(_), .. - }) => { - config_store.save_message(&thread, message)?; - } + }) => config_store.save_message(&thread, message)?, ContentBody::SynchronizeMessage(_) => { debug!("skipping saving sync message without interesting fields") } diff --git a/src/store/mod.rs b/src/store/mod.rs index d7f8af765..2028c00e2 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -164,6 +164,7 @@ pub trait MessageStore { fn save_message(&mut self, thread: &Thread, message: Content) -> Result<(), Error>; /// Delete a single message, identified by its received timestamp from a thread. + #[deprecated = "message deletion is now handled internally"] fn delete_message(&mut self, thread: &Thread, timestamp: u64) -> Result; /// Retrieve a message from a [Thread] by its timestamp. diff --git a/src/store/sled.rs b/src/store/sled.rs index dbc5c9e2c..1246a5720 100644 --- a/src/store/sled.rs +++ b/src/store/sled.rs @@ -749,7 +749,8 @@ impl SenderKeyStore for SledStore { distribution_id ); self.insert(SLED_TREE_SENDER_KEYS, key, record.serialize()?) - .map_err(Error::into_signal_error) + .map_err(Error::into_signal_error)?; + Ok(()) } async fn load_sender_key( @@ -789,12 +790,11 @@ impl MessageStore for SledStore { fn save_message(&mut self, thread: &Thread, message: Content) -> Result<(), Error> { log::trace!( - "Storing a message with thread: {:?}, timestamp: {}", - thread, + "storing a message with thread: {thread}, timestamp: {}", message.metadata.timestamp, ); - let tree = self.messages_thread_tree_name(&thread); + let tree = self.messages_thread_tree_name(thread); let key = message.metadata.timestamp.to_be_bytes(); let proto: ContentProto = message.into(); @@ -897,7 +897,7 @@ impl DoubleEndedIterator for SledMessagesIter { impl ProfilesStore for SledStore { fn save_profile(&mut self, uuid: Uuid, key: ProfileKey, profile: Profile) -> Result<(), Error> { let key = self.profile_key(uuid, key); - self.insert(SLED_TREE_PROFILES, &key, profile) + self.insert(SLED_TREE_PROFILES, key, profile) } fn profile(&self, uuid: Uuid, key: ProfileKey) -> Result, Error> {