From 29f184b4c405bf9a3723d7eaa407b53147cae971 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sun, 7 Feb 2021 15:52:30 +0100 Subject: [PATCH] Strongly type stock strings This changes the internal stock strings API to be more strongly typed, ensuring that the caller can not construct the stock string in the wrong way. The old approach left it to the callers to figure out how a stock string should be created, now each stock string has their specific arguments and callers can not make mistakes. In particular all the subtleties and different ways of calling stock_system_msg() disappear. This could not use a trait for stock strings, as this would not allow having per-message typed arguments. So we needed a type per message with a custom method, only by convention this method is .stock_str(). The type is a enum without variants to avoid allowing someone to create the type. Sadly the fallback string and substitutions are still far away from each other, but it is now only one place which needs to know how to construct the string instead of many. --- src/chat.rs | 141 ++--- src/chatlist.rs | 10 +- src/config.rs | 12 +- src/configure/mod.rs | 15 +- src/contact.rs | 25 +- src/dc_receive_imf.rs | 99 ++- src/dc_tools.rs | 27 +- src/ephemeral.rs | 217 +++---- src/imap/mod.rs | 19 +- src/imex.rs | 9 +- src/location.rs | 16 +- src/message.rs | 73 +-- src/mimefactory.rs | 48 +- src/mimeparser.rs | 8 +- src/peerstate.rs | 6 +- src/securejoin/mod.rs | 27 +- src/smtp/mod.rs | 16 +- src/sql.rs | 20 +- src/stock.rs | 1347 ++++++++++++++++++++++++++++++++++------- 19 files changed, 1488 insertions(+), 647 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 533df33b9..ccffe2fb0 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,6 +1,6 @@ //! # Chat module -use deltachat_derive::{FromSql, ToSql}; +use std::borrow::Cow; use std::convert::TryFrom; use std::str::FromStr; use std::time::{Duration, SystemTime}; @@ -8,6 +8,7 @@ use std::time::{Duration, SystemTime}; use anyhow::Context as _; use anyhow::{bail, ensure, format_err, Error}; use async_std::path::{Path, PathBuf}; +use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; @@ -38,7 +39,11 @@ use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::sql; -use crate::stock::StockMessage; +use crate::stock::{ + ArchivedChats, DeadDrop, DeviceMessages, E2eAvailable, E2ePreferred, EncrNone, MsgAddMember, + MsgDelMember, MsgGroupLeft, MsgGrpImgChanged, MsgGrpImgDeleted, MsgGrpName, NewGroupDraft, + SavedMessages, SelfDeletedMsgBody, VideochatInviteMsgBody, +}; /// An chat item, such as a message or a marker. #[derive(Debug, Copy, Clone)] @@ -396,12 +401,7 @@ impl ChatId { if chat.is_self_talk() { let mut msg = Message::new(Viewtype::Text); - msg.text = Some( - context - .stock_str(StockMessage::SelfDeletedMsgBody) - .await - .into(), - ); + msg.text = Some(SelfDeletedMsgBody::stock_str(context).await.into()); add_device_msg(&context, None, Some(&mut msg)).await?; } @@ -659,23 +659,23 @@ impl ChatId { let addr = contact.get_addr(); let peerstate = Peerstate::from_addr(context, addr).await?; - let stock_message = peerstate + let stock_message = match peerstate .filter(|peerstate| { peerstate .peek_key(PeerstateVerifiedStatus::Unverified) .is_some() }) - .map(|peerstate| match peerstate.prefer_encrypt { - EncryptPreference::Mutual => StockMessage::E2ePreferred, - EncryptPreference::NoPreference => StockMessage::E2eAvailable, - EncryptPreference::Reset => StockMessage::EncrNone, - }) - .unwrap_or(StockMessage::EncrNone); - + .map(|peerstate| peerstate.prefer_encrypt) + { + Some(EncryptPreference::Mutual) => E2ePreferred::stock_str(context).await, + Some(EncryptPreference::NoPreference) => E2eAvailable::stock_str(context).await, + Some(EncryptPreference::Reset) => EncrNone::stock_str(context).await, + None => EncrNone::stock_str(context).await, + }; if !ret.is_empty() { ret.push('\n') } - ret += &format!("{} {}", addr, context.stock_str(stock_message).await); + ret += &format!("{} {}", addr, stock_message); } Ok(ret) @@ -793,9 +793,9 @@ impl Chat { } Ok(mut chat) => { if chat.id.is_deaddrop() { - chat.name = context.stock_str(StockMessage::DeadDrop).await.into(); + chat.name = DeadDrop::stock_str(context).await.into(); } else if chat.id.is_archived_link() { - let tempname = context.stock_str(StockMessage::ArchivedChats).await; + let tempname = ArchivedChats::stock_str(context).await; let cnt = dc_get_archived_cnt(context).await; chat.name = format!("{} ({})", tempname, cnt); } else { @@ -810,9 +810,9 @@ impl Chat { chat.name = chat_name; } if chat.param.exists(Param::Selftalk) { - chat.name = context.stock_str(StockMessage::SavedMessages).await.into(); + chat.name = SavedMessages::stock_str(context).await.into(); } else if chat.param.exists(Param::Devicetalk) { - chat.name = context.stock_str(StockMessage::DeviceMessages).await.into(); + chat.name = DeviceMessages::stock_str(context).await.into(); } } Ok(chat) @@ -1411,10 +1411,10 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<(), Error> { async fn update_special_chat_name( context: &Context, contact_id: u32, - stock_id: StockMessage, + name: Cow<'static, str>, ) -> Result<(), Error> { if let Ok((chat_id, _)) = lookup_by_contact_id(context, contact_id).await { - let name: String = context.stock_str(stock_id).await.into(); + let name: String = name.into(); // the `!= name` condition avoids unneeded writes context .sql @@ -1428,8 +1428,18 @@ async fn update_special_chat_name( } pub(crate) async fn update_special_chat_names(context: &Context) -> Result<(), Error> { - update_special_chat_name(context, DC_CONTACT_ID_DEVICE, StockMessage::DeviceMessages).await?; - update_special_chat_name(context, DC_CONTACT_ID_SELF, StockMessage::SavedMessages).await?; + update_special_chat_name( + context, + DC_CONTACT_ID_DEVICE, + DeviceMessages::stock_str(context).await, + ) + .await?; + update_special_chat_name( + context, + DC_CONTACT_ID_SELF, + SavedMessages::stock_str(context).await, + ) + .await?; Ok(()) } @@ -1812,12 +1822,9 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re let mut msg = Message::new(Viewtype::VideochatInvitation); msg.param.set(Param::WebrtcRoom, &instance); msg.text = Some( - context - .stock_string_repl_str( - StockMessage::VideochatInviteMsgBody, - Message::parse_webrtc_instance(&instance).1, - ) - .await, + VideochatInviteMsgBody::stock_str(context, Message::parse_webrtc_instance(&instance).1) + .await + .to_string(), ); send_msg(context, chat_id, &mut msg).await } @@ -2154,9 +2161,9 @@ pub async fn create_group_chat( let chat_name = improve_single_line_input(chat_name); ensure!(!chat_name.is_empty(), "Invalid chat name"); - let draft_txt = context - .stock_string_repl_str(StockMessage::NewGroupDraft, &chat_name) - .await; + let draft_txt = NewGroupDraft::stock_str(context, &chat_name) + .await + .to_string(); let grpid = dc_create_id(); context.sql.execute( @@ -2334,14 +2341,9 @@ pub(crate) async fn add_contact_to_chat_ex( if chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 0 { msg.viewtype = Viewtype::Text; msg.text = Some( - context - .stock_system_msg( - StockMessage::MsgAddMember, - contact.get_addr(), - "", - DC_CONTACT_ID_SELF as u32, - ) - .await, + MsgAddMember::stock_str(context, contact.get_addr(), DC_CONTACT_ID_SELF) + .await + .to_string(), ); msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set(Param::Arg, contact.get_addr()); @@ -2537,25 +2539,19 @@ pub async fn remove_contact_from_chat( if contact.id == DC_CONTACT_ID_SELF { set_group_explicitly_left(context, chat.grpid).await?; msg.text = Some( - context - .stock_system_msg( - StockMessage::MsgGroupLeft, - "", - "", - DC_CONTACT_ID_SELF, - ) - .await, + MsgGroupLeft::stock_str(context, DC_CONTACT_ID_SELF) + .await + .to_string(), ); } else { msg.text = Some( - context - .stock_system_msg( - StockMessage::MsgDelMember, - contact.get_addr(), - "", - DC_CONTACT_ID_SELF, - ) - .await, + MsgDelMember::stock_str( + context, + contact.get_addr(), + DC_CONTACT_ID_SELF, + ) + .await + .to_string(), ); } msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); @@ -2651,14 +2647,9 @@ pub async fn set_chat_name( if chat.is_promoted() && !chat.is_mailing_list() { msg.viewtype = Viewtype::Text; msg.text = Some( - context - .stock_system_msg( - StockMessage::MsgGrpName, - &chat.name, - &new_name, - DC_CONTACT_ID_SELF, - ) - .await, + MsgGrpName::stock_str(context, &chat.name, &new_name, DC_CONTACT_ID_SELF) + .await + .to_string(), ); msg.param.set_cmd(SystemMessage::GroupNameChanged); if !chat.name.is_empty() { @@ -2716,9 +2707,9 @@ pub async fn set_chat_profile_image( chat.param.remove(Param::ProfileImage); msg.param.remove(Param::Arg); msg.text = Some( - context - .stock_system_msg(StockMessage::MsgGrpImgDeleted, "", "", DC_CONTACT_ID_SELF) - .await, + MsgGrpImgDeleted::stock_str(context, DC_CONTACT_ID_SELF) + .await + .to_string(), ); } else { let image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) { @@ -2734,9 +2725,9 @@ pub async fn set_chat_profile_image( chat.param.set(Param::ProfileImage, image_blob.as_name()); msg.param.set(Param::Arg, image_blob.as_name()); msg.text = Some( - context - .stock_system_msg(StockMessage::MsgGrpImgChanged, "", "", DC_CONTACT_ID_SELF) - .await, + MsgGrpImgChanged::stock_str(context, DC_CONTACT_ID_SELF) + .await + .to_string(), ); } chat.update_param(context).await?; @@ -3215,7 +3206,7 @@ mod tests { assert!(chat.visibility == ChatVisibility::Normal); assert!(!chat.is_device_talk()); assert!(chat.can_send()); - assert_eq!(chat.name, t.stock_str(StockMessage::SavedMessages).await); + assert_eq!(chat.name, SavedMessages::stock_str(&t).await); assert!(chat.get_profile_image(&t).await.is_some()); } @@ -3231,7 +3222,7 @@ mod tests { assert!(chat.visibility == ChatVisibility::Normal); assert!(!chat.is_device_talk()); assert!(!chat.can_send()); - assert_eq!(chat.name, t.stock_str(StockMessage::DeadDrop).await); + assert_eq!(chat.name, DeadDrop::stock_str(&t).await); } #[async_std::test] @@ -3308,7 +3299,7 @@ mod tests { assert!(chat.is_device_talk()); assert!(!chat.is_self_talk()); assert!(!chat.can_send()); - assert_eq!(chat.name, t.stock_str(StockMessage::DeviceMessages).await); + assert_eq!(chat.name, DeviceMessages::stock_str(&t).await); assert!(chat.get_profile_image(&t).await.is_some()); // delete device message, make sure it is not added again diff --git a/src/chatlist.rs b/src/chatlist.rs index 8d080d0d6..6d8f610f1 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -14,7 +14,7 @@ use crate::context::Context; use crate::ephemeral::delete_expired_messages; use crate::lot::Lot; use crate::message::{Message, MessageState, MsgId}; -use crate::stock::StockMessage; +use crate::stock::NoMessages; /// An object representing a single chatlist in memory. /// @@ -385,12 +385,7 @@ impl Chatlist { ret.text2 = None; } else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED { - ret.text2 = Some( - context - .stock_str(StockMessage::NoMessages) - .await - .to_string(), - ); + ret.text2 = Some(NoMessages::stock_str(context).await.to_string()); } else { ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context) .await; @@ -445,6 +440,7 @@ mod tests { use crate::chat::{create_group_chat, ProtectionStatus}; use crate::constants::Viewtype; + use crate::stock::StockMessage; use crate::test_utils::TestContext; #[async_std::test] diff --git a/src/config.rs b/src/config.rs index 654949176..c4aba0357 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; +use crate::blob::BlobObject; use crate::chat::ChatId; use crate::constants::DC_VERSION_STR; use crate::context::Context; @@ -11,11 +12,8 @@ use crate::events::EventType; use crate::job; use crate::message::MsgId; use crate::mimefactory::RECOMMENDED_FILE_SIZE; -use crate::stock::StockMessage; -use crate::{ - blob::BlobObject, - provider::{get_provider_by_id, Provider}, -}; +use crate::provider::{get_provider_by_id, Provider}; +use crate::stock::StatusLine; /// The available configuration keys. #[derive( @@ -175,7 +173,7 @@ impl Context { // Default values match key { - Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()), + Config::Selfstatus => Some(StatusLine::stock_str(self).await.into_owned()), Config::ConfiguredInboxFolder => Some("INBOX".to_owned()), _ => key.get_str("default").map(|s| s.to_string()), } @@ -260,7 +258,7 @@ impl Context { } } Config::Selfstatus => { - let def = self.stock_str(StockMessage::StatusLine).await; + let def = StatusLine::stock_str(self).await; let val = if value.is_none() || value.unwrap() == def { None } else { diff --git a/src/configure/mod.rs b/src/configure/mod.rs index 0cb1a35c8..e23764d18 100644 --- a/src/configure/mod.rs +++ b/src/configure/mod.rs @@ -19,7 +19,7 @@ use crate::message::Message; use crate::oauth2::dc_get_oauth2_addr; use crate::provider::{Protocol, Socket, UsernamePattern}; use crate::smtp::Smtp; -use crate::stock::StockMessage; +use crate::stock::{ConfigurationFailed, ErrorNoNetwork}; use crate::{chat, e2ee, provider}; use crate::{config::Config, dc_tools::time}; use crate::{ @@ -127,12 +127,14 @@ impl Context { self, 0, Some( - self.stock_string_repl_str( - StockMessage::ConfigurationFailed, - // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: + ConfigurationFailed::stock_str( + self, + // We are using Anyhow's .context() and to show the + // inner error, too, we need the {:#}: format!("{:#}", err), ) .await + .to_string() ) ); Err(err) @@ -589,10 +591,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec StockMessage::E2ePreferred, - EncryptPreference::NoPreference => StockMessage::E2eAvailable, - EncryptPreference::Reset => StockMessage::EncrNone, + EncryptPreference::Mutual => E2ePreferred::stock_str(context).await, + EncryptPreference::NoPreference => E2eAvailable::stock_str(context).await, + EncryptPreference::Reset => EncrNone::stock_str(context).await, }; ret += &format!( "{}\n{}:", - context.stock_str(stock_message).await, - context.stock_str(StockMessage::FingerPrints).await + stock_message, + FingerPrints::stock_str(context).await ); let fingerprint_self = SignedPublicKey::load_self(context) @@ -770,7 +767,7 @@ impl Contact { cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, ""); } } else { - ret += &context.stock_str(StockMessage::EncrNone).await; + ret += &EncrNone::stock_str(context).await; } } @@ -1510,7 +1507,7 @@ mod tests { // check SELF let contact = Contact::load_from_db(&t, DC_CONTACT_ID_SELF).await.unwrap(); assert_eq!(DC_CONTACT_ID_SELF, 1); - assert_eq!(contact.get_name(), t.stock_str(StockMessage::SelfMsg).await); + assert_eq!(contact.get_name(), SelfMsg::stock_str(&t).await); assert_eq!(contact.get_addr(), ""); // we're not configured assert!(!contact.is_blocked()); } diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 251692840..c1b15c912 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -26,7 +26,10 @@ use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMess use crate::param::{Param, Params}; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus}; use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device}; -use crate::stock::StockMessage; +use crate::stock::{ + MsgAddMember, MsgDelMember, MsgGroupLeft, MsgGrpImgChanged, MsgGrpImgDeleted, MsgGrpName, + MsgLocationEnabled, UnknownSenderForChat, +}; use crate::{contact, location}; // IndexSet is like HashSet but maintains order of insertion @@ -1165,9 +1168,9 @@ async fn create_or_lookup_group( let mut better_msg: String = From::from(""); if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { - better_msg = context - .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", from_id as u32) - .await; + better_msg = MsgLocationEnabled::stock_str_by(context, from_id) + .await + .to_string(); set_better_msg(mime_parser, &better_msg); } @@ -1231,48 +1234,38 @@ async fn create_or_lookup_group( match removed_id { Some(contact_id) => { mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup; - better_msg = context - .stock_system_msg( - if contact_id == from_id as u32 { - StockMessage::MsgGroupLeft - } else { - StockMessage::MsgDelMember - }, - &removed_addr, - "", - from_id as u32, - ) - .await; + better_msg = if contact_id == from_id { + MsgGroupLeft::stock_str(context, from_id).await.to_string() + } else { + MsgDelMember::stock_str(context, &removed_addr, from_id) + .await + .to_string() + }; } None => warn!(context, "removed {:?} has no contact_id", removed_addr), } } else { let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned(); - if let Some(optional_field) = field { + if let Some(added_member) = field { mime_parser.is_system_message = SystemMessage::MemberAddedToGroup; - better_msg = context - .stock_system_msg( - StockMessage::MsgAddMember, - &optional_field, - "", - from_id as u32, - ) - .await; - X_MrAddToGrp = Some(optional_field); + better_msg = MsgAddMember::stock_str(context, &added_member, from_id) + .await + .to_string(); + X_MrAddToGrp = Some(added_member); } else if let Some(old_name) = mime_parser.get(HeaderDef::ChatGroupNameChanged) { X_MrGrpNameChanged = true; - better_msg = context - .stock_system_msg( - StockMessage::MsgGrpName, - old_name, - if let Some(ref name) = grpname { - name - } else { - "" - }, - from_id as u32, - ) - .await; + better_msg = MsgGrpName::stock_str( + context, + old_name, + if let Some(ref name) = grpname { + name + } else { + "" + }, + from_id as u32, + ) + .await + .to_string(); mime_parser.is_system_message = SystemMessage::GroupNameChanged; } else if let Some(value) = mime_parser.get(HeaderDef::ChatContent) { if value == "group-avatar-changed" { @@ -1280,17 +1273,14 @@ async fn create_or_lookup_group( // this is just an explicit message containing the group-avatar, // apart from that, the group-avatar is send along with various other messages mime_parser.is_system_message = SystemMessage::GroupImageChanged; - better_msg = context - .stock_system_msg( - match avatar_action { - AvatarAction::Delete => StockMessage::MsgGrpImgDeleted, - AvatarAction::Change(_) => StockMessage::MsgGrpImgChanged, - }, - "", - "", - from_id as u32, - ) - .await + better_msg = match avatar_action { + AvatarAction::Delete => MsgGrpImgDeleted::stock_str(context, from_id) + .await + .to_string(), + AvatarAction::Change(_) => MsgGrpImgChanged::stock_str(context, from_id) + .await + .to_string(), + }; } } } @@ -1308,7 +1298,7 @@ async fn create_or_lookup_group( // but still show the message as part of the chat. // After all, the sender has a reference/in-reply-to that // points to this chat. - let s = context.stock_str(StockMessage::UnknownSenderForChat).await; + let s = UnknownSenderForChat::stock_str(context).await; mime_parser.repl_msg_by_error(s.to_string()); } @@ -1989,6 +1979,7 @@ mod tests { use crate::constants::{DC_CONTACT_ID_INFO, DC_GCL_NO_SPECIALS}; use crate::message::ContactRequestDecision::*; use crate::message::Message; + use crate::stock::FailedSendingTo; use crate::test_utils::TestContext; use crate::{ chat::{ChatItem, ChatVisibility}, @@ -2656,11 +2647,9 @@ mod tests { assert_eq!( last_msg.text, Some( - t.stock_string_repl_str( - StockMessage::FailedSendingTo, - "assidhfaaspocwaeofi@gmail.com", - ) - .await, + FailedSendingTo::stock_str(&t, "assidhfaaspocwaeofi@gmail.com") + .await + .to_string(), ) ); assert_eq!(last_msg.from_id, DC_CONTACT_ID_INFO); diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 19503b0df..1cd1d5fd6 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -22,7 +22,7 @@ use crate::context::Context; use crate::events::EventType; use crate::message::Message; use crate::provider::get_provider_update_timestamp; -use crate::stock::StockMessage; +use crate::stock::{BadTimeMsgBody, UpdateReminderMsgBody}; /// Shortens a string to a specified length and adds "[...]" to the /// end of the shortened string. @@ -169,15 +169,15 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam if now < known_past_timestamp { let mut msg = Message::new(Viewtype::Text); msg.text = Some( - context - .stock_string_repl_str( - StockMessage::BadTimeMsgBody, - Local - .timestamp(now, 0) - .format("%Y-%m-%d %H:%M:%S") - .to_string(), - ) - .await, + BadTimeMsgBody::stock_str( + context, + Local + .timestamp(now, 0) + .format("%Y-%m-%d %H:%M:%S") + .to_string(), + ) + .await + .to_string(), ); add_device_msg_with_importance( context, @@ -201,12 +201,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) { if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 { let mut msg = Message::new(Viewtype::Text); - msg.text = Some( - context - .stock_str(StockMessage::UpdateReminderMsgBody) - .await - .into(), - ); + msg.text = Some(UpdateReminderMsgBody::stock_str(context).await.into()); add_device_msg( context, Some( diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 76bd66620..ab2e82996 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -56,7 +56,15 @@ //! the database entries which are expired either according to their //! ephemeral message timers or global `delete_server_after` setting. +use std::borrow::Cow; +use std::convert::{TryFrom, TryInto}; +use std::num::ParseIntError; +use std::str::FromStr; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + use anyhow::{ensure, Error}; +use async_std::task; +use serde::{Deserialize, Serialize}; use crate::chat::{lookup_by_contact_id, send_msg, ChatId}; use crate::constants::{ @@ -68,13 +76,12 @@ use crate::events::EventType; use crate::message::{Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::sql; -use crate::stock::StockMessage; -use async_std::task; -use serde::{Deserialize, Serialize}; -use std::convert::{TryFrom, TryInto}; -use std::num::ParseIntError; -use std::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::stock::{ + MsgEphemeralTimerDay, MsgEphemeralTimerDays, MsgEphemeralTimerDisabled, + MsgEphemeralTimerEnabled, MsgEphemeralTimerHour, MsgEphemeralTimerHours, + MsgEphemeralTimerMinute, MsgEphemeralTimerMinutes, MsgEphemeralTimerWeek, + MsgEphemeralTimerWeeks, +}; #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum Timer { @@ -194,7 +201,11 @@ impl ChatId { } self.inner_set_ephemeral_timer(context, timer).await?; let mut msg = Message::new(Viewtype::Text); - msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await); + msg.text = Some( + stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF) + .await + .to_string(), + ); msg.param.set_cmd(SystemMessage::EphemeralTimerChanged); if let Err(err) = send_msg(context, self, &mut msg).await { error!( @@ -211,87 +222,48 @@ pub(crate) async fn stock_ephemeral_timer_changed( context: &Context, timer: Timer, from_id: u32, -) -> String { +) -> Cow<'static, str> { match timer { - Timer::Disabled => { - context - .stock_system_msg( - StockMessage::MsgEphemeralTimerDisabled, - timer.to_string(), - "", + Timer::Disabled => MsgEphemeralTimerDisabled::stock_str(context, from_id).await, + Timer::Enabled { duration } => match duration { + 0..=59 => { + MsgEphemeralTimerEnabled::stock_str(context, timer.to_string(), from_id).await + } + 60 => MsgEphemeralTimerMinute::stock_str(context, from_id).await, + 61..=3599 => { + MsgEphemeralTimerMinutes::stock_str( + context, + format!("{}", (f64::from(duration) / 6.0).round() / 10.0), from_id, ) .await - } - Timer::Enabled { duration } => match duration { - 0..=59 => { - context - .stock_system_msg( - StockMessage::MsgEphemeralTimerEnabled, - timer.to_string(), - "", - from_id, - ) - .await - } - 60 => { - context - .stock_system_msg(StockMessage::MsgEphemeralTimerMinute, "", "", from_id) - .await - } - 61..=3599 => { - context - .stock_system_msg( - StockMessage::MsgEphemeralTimerMinutes, - format!("{}", (f64::from(duration) / 6.0).round() / 10.0), - "", - from_id, - ) - .await - } - 3600 => { - context - .stock_system_msg(StockMessage::MsgEphemeralTimerHour, "", "", from_id) - .await } + 3600 => MsgEphemeralTimerHour::stock_str(context, from_id).await, 3601..=86399 => { - context - .stock_system_msg( - StockMessage::MsgEphemeralTimerHours, - format!("{}", (f64::from(duration) / 360.0).round() / 10.0), - "", - from_id, - ) - .await - } - 86400 => { - context - .stock_system_msg(StockMessage::MsgEphemeralTimerDay, "", "", from_id) - .await + MsgEphemeralTimerHours::stock_str( + context, + format!("{}", (f64::from(duration) / 360.0).round() / 10.0), + from_id, + ) + .await } + 86400 => MsgEphemeralTimerDay::stock_str(context, from_id).await, 86401..=604_799 => { - context - .stock_system_msg( - StockMessage::MsgEphemeralTimerDays, - format!("{}", (f64::from(duration) / 8640.0).round() / 10.0), - "", - from_id, - ) - .await - } - 604_800 => { - { context.stock_system_msg(StockMessage::MsgEphemeralTimerWeek, "", "", from_id) } - .await + MsgEphemeralTimerDays::stock_str( + context, + format!("{}", (f64::from(duration) / 8640.0).round() / 10.0), + from_id, + ) + .await } + 604_800 => MsgEphemeralTimerWeek::stock_str(context, from_id).await, _ => { - context - .stock_system_msg( - StockMessage::MsgEphemeralTimerWeeks, - format!("{}", (f64::from(duration) / 60480.0).round() / 10.0), - "", - from_id, - ) - .await + MsgEphemeralTimerWeeks::stock_str( + context, + format!("{}", (f64::from(duration) / 60480.0).round() / 10.0), + from_id, + ) + .await } }, } @@ -547,36 +519,67 @@ mod tests { ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await, - "Message deletion timer is disabled." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 1 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 1 s by me." ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await, - "Message deletion timer is set to 1 s." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 30 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 30 s by me." ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await, - "Message deletion timer is set to 30 s." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 60 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 1 minute by me." ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await, - "Message deletion timer is set to 1 minute." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 90 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 1.5 minutes by me." ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, 0).await, - "Message deletion timer is set to 1.5 minutes." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 30 * 60 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 30 minutes by me." ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 * 60 }, 0).await, - "Message deletion timer is set to 30 minutes." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 60 * 60 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 1 hour by me." ); assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await, - "Message deletion timer is set to 1 hour." - ); - assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 5400 }, 0).await, - "Message deletion timer is set to 1.5 hours." + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 5400 }, + DC_CONTACT_ID_SELF + ) + .await, + "Message deletion timer is set to 1.5 hours by me." ); assert_eq!( stock_ephemeral_timer_changed( @@ -584,10 +587,10 @@ mod tests { Timer::Enabled { duration: 2 * 60 * 60 }, - 0 + DC_CONTACT_ID_SELF ) .await, - "Message deletion timer is set to 2 hours." + "Message deletion timer is set to 2 hours by me." ); assert_eq!( stock_ephemeral_timer_changed( @@ -595,10 +598,10 @@ mod tests { Timer::Enabled { duration: 24 * 60 * 60 }, - 0 + DC_CONTACT_ID_SELF ) .await, - "Message deletion timer is set to 1 day." + "Message deletion timer is set to 1 day by me." ); assert_eq!( stock_ephemeral_timer_changed( @@ -606,10 +609,10 @@ mod tests { Timer::Enabled { duration: 2 * 24 * 60 * 60 }, - 0 + DC_CONTACT_ID_SELF ) .await, - "Message deletion timer is set to 2 days." + "Message deletion timer is set to 2 days by me." ); assert_eq!( stock_ephemeral_timer_changed( @@ -617,10 +620,10 @@ mod tests { Timer::Enabled { duration: 7 * 24 * 60 * 60 }, - 0 + DC_CONTACT_ID_SELF ) .await, - "Message deletion timer is set to 1 week." + "Message deletion timer is set to 1 week by me." ); assert_eq!( stock_ephemeral_timer_changed( @@ -628,10 +631,10 @@ mod tests { Timer::Enabled { duration: 4 * 7 * 24 * 60 * 60 }, - 0 + DC_CONTACT_ID_SELF ) .await, - "Message deletion timer is set to 4 weeks." + "Message deletion timer is set to 4 weeks by me." ); } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index f7a248461..f0eab58a1 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -14,12 +14,17 @@ use async_std::channel::Receiver; use async_std::prelude::*; use num_traits::FromPrimitive; +use crate::chat; +use crate::config::Config; use crate::constants::{ Chattype, ShowEmails, Viewtype, DC_CONTACT_ID_SELF, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION, DC_LP_AUTH_OAUTH2, }; use crate::context::Context; -use crate::dc_receive_imf::{from_field_to_contact_id, get_prefetch_parent_message}; +use crate::dc_receive_imf::{ + dc_receive_imf_inner, from_field_to_contact_id, get_prefetch_parent_message, +}; +use crate::dc_tools::dc_extract_grpid_from_rfc724_mid; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::job::{self, Action}; @@ -29,10 +34,8 @@ use crate::mimeparser; use crate::oauth2::dc_get_oauth2_access_token; use crate::param::Params; use crate::provider::Socket; -use crate::{ - chat, dc_tools::dc_extract_grpid_from_rfc724_mid, scheduler::InterruptInfo, stock::StockMessage, -}; -use crate::{config::Config, dc_receive_imf::dc_receive_imf_inner}; +use crate::scheduler::InterruptInfo; +use crate::stock::CannotLogin; mod client; mod idle; @@ -252,9 +255,9 @@ impl Imap { Err((err, _)) => { let imap_user = self.config.lp.user.to_owned(); - let message = context - .stock_string_repl_str(StockMessage::CannotLogin, &imap_user) - .await; + let message = CannotLogin::stock_str(context, &imap_user) + .await + .to_string(); warn!(context, "{} ({})", message, err); diff --git a/src/imex.rs b/src/imex.rs index c8d76a4b8..1b36528c2 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -29,7 +29,7 @@ use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::pgp; use crate::sql::{self, Sql}; -use crate::stock::StockMessage; +use crate::stock::{AcSetupMsgBody, AcSetupMsgSubject}; use ::pgp::types::KeyTrait; use async_tar::Archive; @@ -275,8 +275,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result"); Ok(format!( concat!( @@ -902,8 +902,11 @@ where #[cfg(test)] mod tests { use super::*; + use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE}; + use crate::stock::StockMessage; use crate::test_utils::{alice_keypair, TestContext}; + use ::pgp::armor::BlockType; #[async_std::test] diff --git a/src/location.rs b/src/location.rs index ec0482582..67afda02f 100644 --- a/src/location.rs +++ b/src/location.rs @@ -14,7 +14,7 @@ use crate::job::{self, Job}; use crate::message::{Message, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::Params; -use crate::stock::StockMessage; +use crate::stock::{MsgLocationDisabled, MsgLocationEnabled}; /// Location record #[derive(Debug, Clone, Default)] @@ -212,19 +212,13 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: { if 0 != seconds && !is_sending_locations_before { let mut msg = Message::new(Viewtype::Text); - msg.text = Some( - context - .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0) - .await, - ); + msg.text = Some(MsgLocationEnabled::stock_str(context).await.to_string()); msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); chat::send_msg(context, chat_id, &mut msg) .await .unwrap_or_default(); } else if 0 == seconds && is_sending_locations_before { - let stock_str = context - .stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0) - .await; + let stock_str = MsgLocationDisabled::stock_str(context).await; chat::add_info_msg(context, chat_id, stock_str).await; } context.emit_event(EventType::ChatModified(chat_id)); @@ -716,9 +710,7 @@ pub(crate) async fn job_maybe_send_locations_ended( paramsv![chat_id], ).await); - let stock_str = context - .stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0) - .await; + let stock_str = MsgLocationDisabled::stock_str(context).await; chat::add_info_msg(context, chat_id, stock_str).await; context.emit_event(EventType::ChatModified(chat_id)); } diff --git a/src/message.rs b/src/message.rs index 74b8254f2..887b6f41c 100644 --- a/src/message.rs +++ b/src/message.rs @@ -26,7 +26,10 @@ use crate::lot::{Lot, LotState, Meaning}; use crate::mimeparser::{FailureReport, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::split_armored_data; -use crate::stock::StockMessage; +use crate::stock::{ + AcSetupMsgSubject, Audio, Draft, FailedSendingTo, File, Gif, Image, Location, ReplyNoun, + SelfMsg, Sticker, Video, VideochatInvitation, VoiceMessage, +}; use std::collections::BTreeMap; // In practice, the user additionally cuts the string themselves @@ -1055,26 +1058,14 @@ impl Lot { context: &Context, ) { if msg.state == MessageState::OutDraft { - self.text1 = Some( - context - .stock_str(StockMessage::Draft) - .await - .to_owned() - .into(), - ); + self.text1 = Some(Draft::stock_str(context).await.to_owned().into()); self.text1_meaning = Meaning::Text1Draft; } else if msg.from_id == DC_CONTACT_ID_SELF { if msg.is_info() || chat.is_self_talk() { self.text1 = None; self.text1_meaning = Meaning::None; } else { - self.text1 = Some( - context - .stock_str(StockMessage::SelfMsg) - .await - .to_owned() - .into(), - ); + self.text1 = Some(SelfMsg::stock_str(context).await.to_owned().into()); self.text1_meaning = Meaning::Text1Self; } } else { @@ -1107,10 +1098,7 @@ impl Lot { .await; if text2.is_empty() && msg.quoted_text().is_some() { - text2 = context - .stock_str(StockMessage::ReplyNoun) - .await - .into_owned() + text2 = ReplyNoun::stock_str(context).await.into_owned() } self.text2 = Some(text2); @@ -1562,21 +1550,15 @@ pub async fn get_summarytext_by_raw( ) -> String { let mut append_text = true; let prefix = match viewtype { - Viewtype::Image => context.stock_str(StockMessage::Image).await.into_owned(), - Viewtype::Gif => context.stock_str(StockMessage::Gif).await.into_owned(), - Viewtype::Sticker => context.stock_str(StockMessage::Sticker).await.into_owned(), - Viewtype::Video => context.stock_str(StockMessage::Video).await.into_owned(), - Viewtype::Voice => context - .stock_str(StockMessage::VoiceMessage) - .await - .into_owned(), + Viewtype::Image => Image::stock_str(context).await.into_owned(), + Viewtype::Gif => Gif::stock_str(context).await.into_owned(), + Viewtype::Sticker => Sticker::stock_str(context).await.into_owned(), + Viewtype::Video => Video::stock_str(context).await.into_owned(), + Viewtype::Voice => VoiceMessage::stock_str(context).await.into_owned(), Viewtype::Audio | Viewtype::File => { if param.get_cmd() == SystemMessage::AutocryptSetupMessage { append_text = false; - context - .stock_str(StockMessage::AcSetupMsgSubject) - .await - .to_string() + AcSetupMsgSubject::stock_str(context).await.to_string() } else { let file_name: String = param .get_path(Param::File, context) @@ -1586,29 +1568,24 @@ pub async fn get_summarytext_by_raw( .map(|fname| fname.to_string_lossy().into_owned()) }) .unwrap_or_else(|| String::from("ErrFileName")); - let label = context - .stock_str(if viewtype == Viewtype::Audio { - StockMessage::Audio - } else { - StockMessage::File - }) - .await; + let label = if viewtype == Viewtype::Audio { + Audio::stock_str(context).await + } else { + File::stock_str(context).await + }; format!("{} – {}", label, file_name) } } Viewtype::VideochatInvitation => { append_text = false; - context - .stock_str(StockMessage::VideochatInvitation) - .await - .into_owned() + VideochatInvitation::stock_str(context).await.into_owned() } _ => { if param.get_cmd() != SystemMessage::LocationOnly { "".to_string() } else { append_text = false; - context.stock_str(StockMessage::Location).await.to_string() + Location::stock_str(context).await.to_string() } } }; @@ -1865,13 +1842,9 @@ async fn ndn_maybe_add_info_msg( Error::msg("ndn_maybe_add_info_msg: Contact ID not found") })?; let contact = Contact::load_from_db(context, contact_id).await?; - // Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear) - let text = context - .stock_string_repl_str( - StockMessage::FailedSendingTo, - contact.get_display_name(), - ) - .await; + // Tell the user which of the recipients failed if we know that (because in + // a group, this might otherwise be unclear) + let text = FailedSendingTo::stock_str(context, contact.get_display_name()).await; chat::add_info_msg(context, chat_id, text).await; context.emit_event(EventType::ChatModified(chat_id)); } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 8aae58269..1bec369f4 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -21,7 +21,10 @@ use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::simplify::escape_message_footer_marks; -use crate::stock::StockMessage; +use crate::stock::{ + AcSetupMsgBody, AcSetupMsgSubject, EncryptedMsg, ReadRcpt, ReadRcptMailBody, StatusLine, + SubjectForNewContact, +}; use std::convert::TryInto; // attachments of 25 mb brutto should work on the majority of providers @@ -139,10 +142,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { ) .await?; - let default_str = context - .stock_str(StockMessage::StatusLine) - .await - .to_string(); + let default_str = StatusLine::stock_str(context).await.to_string(); let factory = MimeFactory { from_addr, from_displayname, @@ -180,10 +180,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .get_config(Config::Displayname) .await .unwrap_or_default(); - let default_str = context - .stock_str(StockMessage::StatusLine) - .await - .to_string(); + let default_str = StatusLine::stock_str(context).await.to_string(); let selfstatus = context .get_config(Config::Selfstatus) .await @@ -345,8 +342,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { match self.loaded { Loaded::Message { ref chat } => { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { - self.context - .stock_str(StockMessage::AcSetupMsgSubject) + AcSetupMsgSubject::stock_str(self.context) .await .into_owned() } else if chat.typ == Chattype::Group { @@ -390,21 +386,14 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .unwrap_or_default(), }; - self.context - .stock_string_repl_str( - StockMessage::SubjectForNewContact, - self_name, - ) + SubjectForNewContact::stock_str(self.context, self_name) .await + .to_string() } } } } - Loaded::MDN { .. } => self - .context - .stock_str(StockMessage::ReadRcpt) - .await - .into_owned(), + Loaded::MDN { .. } => ReadRcpt::stock_str(self.context).await.into_owned(), } } @@ -808,12 +797,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { unprotected_headers .push(Header::new("Autocrypt-Setup-Message".into(), "v1".into())); - placeholdertext = Some( - self.context - .stock_str(StockMessage::AcSetupMsgBody) - .await - .to_string(), - ); + placeholdertext = Some(AcSetupMsgBody::stock_str(self.context).await.to_string()); } SystemMessage::SecurejoinMessage => { let msg = &self.msg; @@ -1071,17 +1055,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .get_int(Param::GuaranteeE2ee) .unwrap_or_default() { - self.context - .stock_str(StockMessage::EncryptedMsg) - .await - .into_owned() + EncryptedMsg::stock_str(self.context).await.into_owned() } else { self.msg.get_summarytext(self.context, 32).await }; - let p2 = self - .context - .stock_string_repl_str(StockMessage::ReadRcptMailBody, p1) - .await; + let p2 = ReadRcptMailBody::stock_str(self.context, p1).await; let message_text = format!("{}\r\n", p2); message = message.child( PartBuilder::new() diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 1567a3aa3..a2436f9e1 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -3,10 +3,12 @@ use std::future::Future; use std::pin::Pin; use anyhow::{bail, Result}; +use charset::Charset; use deltachat_derive::{FromSql, ToSql}; use lettre_email::mime::{self, Mime}; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use once_cell::sync::Lazy; +use percent_encoding::percent_decode_str; use crate::aheader::Aheader; use crate::blob::BlobObject; @@ -25,9 +27,7 @@ use crate::message; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::simplify::simplify; -use crate::stock::StockMessage; -use charset::Charset; -use percent_encoding::percent_decode_str; +use crate::stock::CantDecryptMsgBody; /// A parsed MIME message. /// @@ -629,7 +629,7 @@ impl MimeMessage { // we currently do not try to decrypt non-autocrypt messages // at all. If we see an encrypted part, we set // decrypting_failed. - let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody).await; + let msg_body = CantDecryptMsgBody::stock_str(context).await; let txt = format!("[{}]", msg_body); let part = Part { diff --git a/src/peerstate.rs b/src/peerstate.rs index e6bf43c79..f2f9e33e2 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -13,7 +13,7 @@ use crate::context::Context; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::sql::Sql; -use crate::stock::StockMessage; +use crate::stock::ContactSetupChanged; #[derive(Debug)] pub enum PeerstateKeyType { @@ -281,9 +281,7 @@ impl<'a> Peerstate<'a> { .await .unwrap_or_default(); - let msg = context - .stock_string_repl_str(StockMessage::ContactSetupChanged, self.addr.clone()) - .await; + let msg = ContactSetupChanged::stock_str(context, self.addr.clone()).await; chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, EventType::ChatModified(contact_chat_id)); diff --git a/src/securejoin/mod.rs b/src/securejoin/mod.rs index 7f32d5857..efc11b812 100644 --- a/src/securejoin/mod.rs +++ b/src/securejoin/mod.rs @@ -23,7 +23,7 @@ use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave}; use crate::qr::check_qr; use crate::sql; -use crate::stock::StockMessage; +use crate::stock::{ContactNotVerified, ContactVerified}; use crate::token; mod bobstate; @@ -822,10 +822,8 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI } else { "?" }; - let msg = context - .stock_string_repl_str(StockMessage::ContactVerified, addr) - .await; - chat::add_info_msg(context, contact_chat_id, &msg).await; + let msg = ContactVerified::stock_str(context, addr).await; + chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, EventType::ChatModified(contact_chat_id)); info!(context, "StockMessage::ContactVerified posted to 1:1 chat"); } @@ -837,16 +835,15 @@ async fn could_not_establish_secure_connection( ) { let contact_id = chat_id_2_contact_id(context, contact_chat_id).await; let contact = Contact::get_by_id(context, contact_id).await; - let msg = context - .stock_string_repl_str( - StockMessage::ContactNotVerified, - if let Ok(ref contact) = contact { - contact.get_addr() - } else { - "?" - }, - ) - .await; + let msg = ContactNotVerified::stock_str( + context, + if let Ok(ref contact) = contact { + contact.get_addr() + } else { + "?" + }, + ) + .await; chat::add_info_msg(context, contact_chat_id, &msg).await; error!( diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs index 6b0a4f1e3..3959d2f03 100644 --- a/src/smtp/mod.rs +++ b/src/smtp/mod.rs @@ -13,7 +13,7 @@ use crate::events::EventType; use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam}; use crate::oauth2::dc_get_oauth2_access_token; use crate::provider::Socket; -use crate::stock::StockMessage; +use crate::stock::ServerResponse; /// SMTP write and read timeout in seconds. const SMTP_TIMEOUT: u64 = 30; @@ -111,13 +111,13 @@ impl Smtp { ) .await; if let Err(ref err) = res { - let message = context - .stock_string_repl_str2( - StockMessage::ServerResponse, - format!("SMTP {}:{}", lp.smtp.server, lp.smtp.port), - err.to_string(), - ) - .await; + let message = ServerResponse::stock_str( + context, + format!("SMTP {}:{}", lp.smtp.server, lp.smtp.port), + err.to_string(), + ) + .await + .to_string(); context.emit_event(EventType::ErrorNetwork(message)); }; diff --git a/src/sql.rs b/src/sql.rs index dffaff619..3605cdaac 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -10,22 +10,19 @@ use std::time::Duration; use anyhow::format_err; use rusqlite::{Connection, Error as SqlError, OpenFlags}; -use crate::chat::add_device_msg; +use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; +use crate::config::Config; use crate::config::Config::DeleteServerAfter; -use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH}; +use crate::constants::{ShowEmails, Viewtype, DC_CHAT_ID_TRASH}; use crate::context::Context; use crate::dc_tools::{dc_delete_file, time, EmailAddress}; use crate::ephemeral::start_ephemeral_timers; use crate::imap; +use crate::message::Message; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::provider::get_provider_by_domain; -use crate::stock::StockMessage; -use crate::{ - chat::{update_device_icon, update_saved_messages_icon}, - config::Config, -}; -use crate::{constants::Viewtype, message::Message}; +use crate::stock::DeleteServerTurnedOff; #[macro_export] macro_rules! paramsv { @@ -1544,12 +1541,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label); // So, for people who have delete_server enabled, disable it and add a hint to the devicechat: if context.get_config_delete_server_after().await.is_some() { let mut msg = Message::new(Viewtype::Text); - msg.text = Some( - context - .stock_str(StockMessage::DeleteServerTurnedOff) - .await - .into(), - ); + msg.text = Some(DeleteServerTurnedOff::stock_str(context).await.into()); add_device_msg(context, None, Some(&mut msg)).await?; context.set_config(DeleteServerAfter, Some("0")).await?; } diff --git a/src/stock.rs b/src/stock.rs index f8e655f37..a70259719 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -6,15 +6,15 @@ use anyhow::{bail, Error}; use strum::EnumProperty; use strum_macros::EnumProperty; +use crate::blob::BlobObject; use crate::chat; use crate::chat::ProtectionStatus; +use crate::config::Config; use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::message::Message; use crate::param::Param; -use crate::stock::StockMessage::{DeviceMessagesHint, WelcomeMessage}; -use crate::{blob::BlobObject, config::Config}; /// Stock strings /// @@ -263,10 +263,6 @@ pub enum StockMessage { MsgEphemeralTimerWeeks = 96, } -/* -" -*/ - impl StockMessage { /// Default untranslated strings for stock messages. /// @@ -276,6 +272,1129 @@ impl StockMessage { } } +/// Builder for a stock string. +/// +/// See [`NoMessages`] or any other stock string in this module for an example of how to use +/// this. +struct StockString<'a> { + context: &'a Context, +} + +impl<'a> StockString<'a> { + /// Creates a new [`StockString`] builder. + fn new(context: &'a Context) -> Self { + Self { context } + } + + /// Looks up a translation and returns a further builder. + /// + /// This will look up the translation in the [`Context`] if one is registered. It + /// returns a further builder type which can be used to substitute replacement strings + /// or build the final message. + async fn id(self, id: StockMessage) -> TranslatedStockString<'a> { + TranslatedStockString { + context: self.context, + message: self + .context + .translated_stockstrings + .read() + .await + .get(&(id as usize)) + .map(|s| Cow::Owned(s.to_owned())) + .unwrap_or_else(|| Cow::Borrowed(id.fallback())), + } + } +} + +/// Stock string builder which allows retrieval of the message. +/// +/// This builder allows retrieval of the message using [`TranslatedStockString::msg`], if it +/// needs substitutions first however it provides further builder methods. +struct TranslatedStockString<'a> { + context: &'a Context, + message: Cow<'static, str>, +} + +impl<'a> TranslatedStockString<'a> { + /// Retrieves the built message. + fn msg(self) -> Cow<'static, str> { + self.message + } + + /// Substitutes the first replacement value if one is present. + fn replace1(self, replacement: impl AsRef) -> Self { + let msg = self + .message + .as_ref() + .replacen("%1$s", replacement.as_ref(), 1) + .replacen("%1$d", replacement.as_ref(), 1) + .replacen("%1$@", replacement.as_ref(), 1); + Self { + context: self.context, + message: Cow::Owned(msg), + } + } + + /// Substitutes the second replacement value if one is present. + /// + /// Be aware you probably should have also called [`TranslatedStockString::replace1`] if + /// you are calling this. + fn replace2(self, replacement: impl AsRef) -> Self { + let msg = self + .message + .as_ref() + .replacen("%2$s", replacement.as_ref(), 1) + .replacen("%2$d", replacement.as_ref(), 1) + .replacen("%2$@", replacement.as_ref(), 1); + Self { + context: self.context, + message: Cow::Owned(msg), + } + } + + /// Augments the message by saying it was performed by a user. + /// + /// This looks up the display name of `contact` and uses the [`MsgActionByMe`] and + /// [`MsgActionByUser`] stock strings to turn the stock string in one that says the + /// action was performed by this user. + /// + /// E.g. this turns `Group image changed.` into `Group image changed by me.` or `Group + /// image changed by Alice`. + /// + /// Note that the original message should end in a `.`. + async fn action_by_contact(self, contact: u32) -> TranslatedStockString<'a> { + let message = self.message.as_ref().trim_end_matches('.'); + let message = match contact { + DC_CONTACT_ID_SELF => MsgActionByMe::stock_str(self.context, message).await, + _ => { + let displayname = Contact::get_by_id(self.context, contact) + .await + .map(|contact| contact.get_name_n_addr()) + .unwrap_or_else(|_| format!("{}", contact)); + MsgActionByUser::stock_str(self.context, message, displayname).await + } + }; + TranslatedStockString { + context: self.context, + message, + } + } +} + +#[derive(Debug)] +pub(crate) enum NoMessages {} + +impl NoMessages { + /// Stock string: `No messages.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::NoMessages) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum SelfMsg {} + +impl SelfMsg { + /// Stock string: `Me`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::SelfMsg) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Draft {} + +impl Draft { + /// Stock string: `Draft`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::Draft) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum VoiceMessage {} + +impl VoiceMessage { + /// Stock string: `Voice message`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::VoiceMessage) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum DeadDrop {} + +impl DeadDrop { + /// Stock string: `Contact requests`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::DeadDrop) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Image {} + +impl Image { + /// Stock string: `Image`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::Image) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Video {} + +impl Video { + /// Stock string: `Video`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::Video) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Audio {} + +impl Audio { + /// Stock string: `Audio`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::Audio) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum File {} + +impl File { + /// Stock string: `File`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context).id(StockMessage::File).await.msg() + } +} + +#[derive(Debug)] +pub(crate) enum StatusLine {} + +impl StatusLine { + /// Stock string: `Sent with my Delta Chat Messenger: https://delta.chat`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::StatusLine) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum NewGroupDraft {} + +impl NewGroupDraft { + /// Stock string: `Hello, I've just created the group "%1$s" for us.`. + pub async fn stock_str(context: &Context, group_name: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::NewGroupDraft) + .await + .replace1(group_name) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgGrpName {} + +impl MsgGrpName { + /// Stock string: `Group name changed from "%1$s" to "%2$s".`. + pub async fn stock_str( + context: &Context, + from_group: impl AsRef, + to_group: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgGrpName) + .await + .replace1(from_group) + .replace2(to_group) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgGrpImgChanged {} + +impl MsgGrpImgChanged { + /// Stock string: `Group image changed.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgGrpImgChanged) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgAddMember {} + +impl MsgAddMember { + /// Stock string: `Member %1$s added.`. + /// + /// The `added_member_addr` parameter should be an email address and is looked up in the + /// contacts to combine with the display name. + pub async fn stock_str( + context: &Context, + added_member_addr: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + let addr = added_member_addr.as_ref(); + let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { + Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) + .await + .map(|contact| contact.get_name_n_addr()) + .unwrap_or_else(|_| addr.to_string()), + _ => addr.to_string(), + }; + StockString::new(context) + .id(StockMessage::MsgAddMember) + .await + .replace1(who) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgDelMember {} + +impl MsgDelMember { + /// Stock string: `Member %1$s removed.`. + /// + /// The `removed_member_addr` parameter should be an email address and is looked up in + /// the contacts to combine with the display name. + pub async fn stock_str( + context: &Context, + removed_member_addr: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + let addr = removed_member_addr.as_ref(); + let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { + Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) + .await + .map(|contact| contact.get_name_n_addr()) + .unwrap_or_else(|_| addr.to_string()), + _ => addr.to_string(), + }; + StockString::new(context) + .id(StockMessage::MsgDelMember) + .await + .replace1(who) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgGroupLeft {} + +impl MsgGroupLeft { + /// Stock string: `Group left.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgGroupLeft) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Gif {} + +impl Gif { + /// Stock string: `GIF`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context).id(StockMessage::Gif).await.msg() + } +} + +#[derive(Debug)] +pub(crate) enum EncryptedMsg {} + +impl EncryptedMsg { + /// Stock string: `Encrypted message`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::EncryptedMsg) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum E2eAvailable {} + +impl E2eAvailable { + /// Stock string: `End-to-end encryption available.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::E2eAvailable) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum EncrNone {} + +impl EncrNone { + /// Stock string: `No encryption.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::EncrNone) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum CantDecryptMsgBody {} + +impl CantDecryptMsgBody { + /// Stock string: `This message was encrypted for another setup.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::CantDecryptMsgBody) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum FingerPrints {} + +impl FingerPrints { + /// Stock string: `Fingerprints`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::FingerPrints) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ReadRcpt {} + +impl ReadRcpt { + /// Stock string: `Return receipt`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ReadRcpt) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ReadRcptMailBody {} + +impl ReadRcptMailBody { + /// Stock string: `This is a return receipt for the message "%1$s".`. + pub async fn stock_str(context: &Context, message: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ReadRcptMailBody) + .await + .replace1(message) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgGrpImgDeleted {} + +impl MsgGrpImgDeleted { + /// Stock string: `Group image deleted.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgGrpImgDeleted) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum E2ePreferred {} + +impl E2ePreferred { + /// Stock string: `End-to-end encryption preferred.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::E2ePreferred) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ContactVerified {} + +impl ContactVerified { + /// Stock string: `%1$s verified.`. + pub async fn stock_str(context: &Context, contact_addr: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ContactVerified) + .await + .replace1(contact_addr) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ContactNotVerified {} + +impl ContactNotVerified { + /// Stock string: `Cannot verify %1$s`. + pub async fn stock_str(context: &Context, contact_addr: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ContactNotVerified) + .await + .replace1(contact_addr) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ContactSetupChanged {} + +impl ContactSetupChanged { + /// Stock string: `Changed setup for %1$s`. + pub async fn stock_str(context: &Context, contact_addr: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ContactSetupChanged) + .await + .replace1(contact_addr) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ArchivedChats {} + +impl ArchivedChats { + /// Stock string: `Archived chats`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ArchivedChats) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum AcSetupMsgSubject {} + +impl AcSetupMsgSubject { + /// Stock string: `Autocrypt Setup Message`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::AcSetupMsgSubject) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum AcSetupMsgBody {} + +impl AcSetupMsgBody { + /// Stock string: `This is the Autocrypt Setup Message used to transfer...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::AcSetupMsgBody) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum CannotLogin {} + +impl CannotLogin { + /// Stock string: `Cannot login as \"%1$s\". Please check...`. + pub async fn stock_str(context: &Context, user: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::CannotLogin) + .await + .replace1(user) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ServerResponse {} + +impl ServerResponse { + /// Stock string: `Could not connect to %1$s: %2$s`. + pub async fn stock_str( + context: &Context, + server: impl AsRef, + details: impl AsRef, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ServerResponse) + .await + .replace1(server) + .replace2(details) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgActionByUser {} + +impl MsgActionByUser { + /// Stock string: `%1$s by %2$s.`. + pub async fn stock_str( + context: &Context, + action: impl AsRef, + user: impl AsRef, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgActionByUser) + .await + .replace1(action) + .replace2(user) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgActionByMe {} + +impl MsgActionByMe { + /// Stock string: `%1$s by me.`. + pub async fn stock_str(context: &Context, action: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgActionByMe) + .await + .replace1(action) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgLocationEnabled {} + +impl MsgLocationEnabled { + /// Stock string: `Location streaming enabled.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgLocationEnabled) + .await + .msg() + } + + /// Stock string: `Location streaming enabled.`. + pub async fn stock_str_by(context: &Context, contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgLocationEnabled) + .await + .action_by_contact(contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgLocationDisabled {} + +impl MsgLocationDisabled { + /// Stock string: `Location streaming disabled.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgLocationDisabled) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Location {} + +impl Location { + /// Stock string: `Location`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::Location) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum Sticker {} + +impl Sticker { + /// Stock string: `Sticker`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::Sticker) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum DeviceMessages {} + +impl DeviceMessages { + /// Stock string: `Device messages`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::DeviceMessages) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum SavedMessages {} + +impl SavedMessages { + /// Stock string: `Saved messages`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::SavedMessages) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum DeviceMessagesHint {} + +impl DeviceMessagesHint { + /// Stock string: `Messages in this chat are generated locally by...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::DeviceMessagesHint) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum WelcomeMessage {} + +impl WelcomeMessage { + /// Stock string: `Welcome to Delta Chat! – ...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::WelcomeMessage) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum UnknownSenderForChat {} + +impl UnknownSenderForChat { + /// Stock string: `Unknown sender for this chat. See 'info' for more details.`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::UnknownSenderForChat) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum SubjectForNewContact {} + +impl SubjectForNewContact { + /// Stock string: `Message from %1$s`. + // TODO: This can compute `self_name` itself instead of asking the caller to do this. + pub async fn stock_str(context: &Context, self_name: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::SubjectForNewContact) + .await + .replace1(self_name) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum FailedSendingTo {} + +impl FailedSendingTo { + /// Stock string: `Failed to send message to %1$s.`. + pub async fn stock_str(context: &Context, name: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::FailedSendingTo) + .await + .replace1(name) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerDisabled {} + +impl MsgEphemeralTimerDisabled { + /// Stock string: `Message deletion timer is disabled.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerDisabled) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerEnabled {} + +impl MsgEphemeralTimerEnabled { + /// Stock string: `Message deletion timer is set to %1$s s.`. + pub async fn stock_str( + context: &Context, + timer: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerEnabled) + .await + .replace1(timer) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerMinute {} + +impl MsgEphemeralTimerMinute { + /// Stock string: `Message deletion timer is set to 1 minute.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerMinute) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerHour {} + +impl MsgEphemeralTimerHour { + /// Stock string: `Message deletion timer is set to 1 hour.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerHour) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerDay {} + +impl MsgEphemeralTimerDay { + /// Stock string: `Message deletion timer is set to 1 day.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerDay) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerWeek {} + +impl MsgEphemeralTimerWeek { + /// Stock string: `Message deletion timer is set to 1 week.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerWeek) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum VideochatInvitation {} + +impl VideochatInvitation { + /// Stock string: `Video chat invitation`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::VideochatInvitation) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum VideochatInviteMsgBody {} + +impl VideochatInviteMsgBody { + /// Stock string: `You are invited to a video chat, click %1$s to join.`. + pub async fn stock_str(context: &Context, url: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::VideochatInviteMsgBody) + .await + .replace1(url) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ConfigurationFailed {} + +impl ConfigurationFailed { + /// Stock string: `Error:\n\n“%1$s”`. + pub async fn stock_str(context: &Context, details: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ConfigurationFailed) + .await + .replace1(details) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum BadTimeMsgBody {} + +impl BadTimeMsgBody { + /// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`. + // TODO: This could compute now itself. + pub async fn stock_str(context: &Context, now: impl AsRef) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::BadTimeMsgBody) + .await + .replace1(now) + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum UpdateReminderMsgBody {} + +impl UpdateReminderMsgBody { + /// Stock string: `⚠️ Your Delta Chat version might be outdated...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::UpdateReminderMsgBody) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ErrorNoNetwork {} + +impl ErrorNoNetwork { + /// Stock string: `Could not find your mail server...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ErrorNoNetwork) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ProtectionEnabled {} + +impl ProtectionEnabled { + /// Stock string: `Chat protection enabled.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ProtectionEnabled) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ProtectionDisabled {} + +impl ProtectionDisabled { + /// Stock string: `Chat protection disabled.`. + pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ProtectionDisabled) + .await + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum ReplyNoun {} + +impl ReplyNoun { + /// Stock string: `Reply`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::ReplyNoun) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum SelfDeletedMsgBody {} + +impl SelfDeletedMsgBody { + /// Stock string: `You deleted the \"Saved messages\" chat...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::SelfDeletedMsgBody) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum DeleteServerTurnedOff {} + +impl DeleteServerTurnedOff { + /// Stock string: `⚠️ The "Delete messages from server" feature now also...`. + pub async fn stock_str(context: &Context) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::DeleteServerTurnedOff) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerMinutes {} + +impl MsgEphemeralTimerMinutes { + /// Stock string: `Message deletion timer is set to %1$s minutes.`. + pub async fn stock_str( + context: &Context, + minutes: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerMinutes) + .await + .replace1(minutes) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerHours {} + +impl MsgEphemeralTimerHours { + /// Stock string: `Message deletion timer is set to %1$s hours.`. + pub async fn stock_str( + context: &Context, + hours: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerHours) + .await + .replace1(hours) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerDays {} + +impl MsgEphemeralTimerDays { + /// Stock string: `Message deletion timer is set to %1$s days.`. + pub async fn stock_str( + context: &Context, + days: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerDays) + .await + .replace1(days) + .action_by_contact(by_contact) + .await + .msg() + } +} + +#[derive(Debug)] +pub(crate) enum MsgEphemeralTimerWeeks {} + +impl MsgEphemeralTimerWeeks { + /// Stock string: `Message deletion timer is set to %1$s weeks.`. + pub async fn stock_str( + context: &Context, + weeks: impl AsRef, + by_contact: u32, + ) -> Cow<'static, str> { + StockString::new(context) + .id(StockMessage::MsgEphemeralTimerWeeks) + .await + .replace1(weeks) + .action_by_contact(by_contact) + .await + .msg() + } +} + impl Context { /// Set the stock string for the [StockMessage]. /// @@ -305,136 +1424,17 @@ impl Context { Ok(()) } - /// Return the stock string for the [StockMessage]. - /// - /// Return a translation (if it was set with set_stock_translation before) - /// or a default (English) string. - pub async fn stock_str(&self, id: StockMessage) -> Cow<'_, str> { - match self - .translated_stockstrings - .read() - .await - .get(&(id as usize)) - { - Some(ref x) => Cow::Owned((*x).to_string()), - None => Cow::Borrowed(id.fallback()), - } - } - - /// Return stock string, replacing placeholders with provided string. - /// - /// This replaces both the *first* `%1$s`, `%1$d` and `%1$@` - /// placeholders with the provided string. - /// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop) - pub async fn stock_string_repl_str(&self, id: StockMessage, insert: impl AsRef) -> String { - self.stock_str(id) - .await - .replacen("%1$s", insert.as_ref(), 1) - .replacen("%1$d", insert.as_ref(), 1) - .replacen("%1$@", insert.as_ref(), 1) - } - - /// Return stock string, replacing placeholders with provided int. - /// - /// Like [Context::stock_string_repl_str] but substitute the placeholders - /// with an integer. - pub async fn stock_string_repl_int(&self, id: StockMessage, insert: i32) -> String { - self.stock_string_repl_str(id, format!("{}", insert).as_str()) - .await - } - - /// Return stock string, replacing 2 placeholders with provided string. - /// - /// This replaces both the *first* `%1$s`, `%1$d` and `%1$@` - /// placeholders with the string in `insert` and does the same for - /// `%2$s`, `%2$d` and `%2$@` for `insert2`. - /// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop) - pub async fn stock_string_repl_str2( + /// Returns a stock message saying that protection status has changed. + pub(crate) async fn stock_protection_msg( &self, - id: StockMessage, - insert: impl AsRef, - insert2: impl AsRef, - ) -> String { - self.stock_str(id) - .await - .replacen("%1$s", insert.as_ref(), 1) - .replacen("%1$d", insert.as_ref(), 1) - .replacen("%1$@", insert.as_ref(), 1) - .replacen("%2$s", insert2.as_ref(), 1) - .replacen("%2$d", insert2.as_ref(), 1) - .replacen("%2$@", insert2.as_ref(), 1) - } - - /// Return some kind of stock message - /// - /// If the `id` is [StockMessage::MsgAddMember] or - /// [StockMessage::MsgDelMember] then `param1` is considered to be the - /// contact address and will be replaced by that contact's display - /// name. - /// - /// If `from_id` is not `0`, any trailing dot is removed from the - /// first stock string created so far. If the `from_id` contact is - /// the user itself, i.e. `DC_CONTACT_ID_SELF` the string is used - /// itself as param to the [StockMessage::MsgActionByMe] stock string - /// resulting in a string like "Member Alice added by me." (for - /// [StockMessage::MsgAddMember] as `id`). If the `from_id` contact - /// is any other user than the contact's display name is looked up and - /// used as the second parameter to [StockMessage::MsgActionByUser] with - /// again the original stock string being used as the first parameter, - /// resulting in a string like "Member Alice added by Bob.". - pub async fn stock_system_msg( - &self, - id: StockMessage, - param1: impl AsRef, - param2: impl AsRef, + protect: ProtectionStatus, from_id: u32, ) -> String { - let insert1 = if matches!(id, StockMessage::MsgAddMember | StockMessage::MsgDelMember) { - match Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown).await { - Ok(Some(contact_id)) => Contact::get_by_id(self, contact_id) - .await - .map(|contact| contact.get_name_n_addr()) - .unwrap_or_else(|_| param1.as_ref().to_string()), - _ => param1.as_ref().to_string(), - } - } else { - param1.as_ref().to_string() - }; - - let action = self - .stock_string_repl_str2(id, insert1, param2.as_ref().to_string()) - .await; - let action1 = action.trim_end_matches('.'); - match from_id { - 0 => action, - DC_CONTACT_ID_SELF => { - self.stock_string_repl_str(StockMessage::MsgActionByMe, action1) - .await - } - _ => { - let displayname = Contact::get_by_id(self, from_id) - .await - .map(|contact| contact.get_name_n_addr()) - .unwrap_or_default(); - - self.stock_string_repl_str2(StockMessage::MsgActionByUser, action1, &displayname) - .await - } + match protect { + ProtectionStatus::Unprotected => ProtectionEnabled::stock_str(self, from_id).await, + ProtectionStatus::Protected => ProtectionDisabled::stock_str(self, from_id).await, } - } - - /// Returns a stock message saying that protection status has changed. - pub async fn stock_protection_msg(&self, protect: ProtectionStatus, from_id: u32) -> String { - self.stock_system_msg( - match protect { - ProtectionStatus::Protected => StockMessage::ProtectionEnabled, - ProtectionStatus::Unprotected => StockMessage::ProtectionDisabled, - }, - "", - "", - from_id, - ) - .await + .to_string() } pub(crate) async fn update_device_chats(&self) -> Result<(), Error> { @@ -454,7 +1454,7 @@ impl Context { // add welcome-messages. by the label, this is done only once, // if the user has deleted the message or the chat, it is not added again. let mut msg = Message::new(Viewtype::Text); - msg.text = Some(self.stock_str(DeviceMessagesHint).await.to_string()); + msg.text = Some(DeviceMessagesHint::stock_str(self).await.to_string()); chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg)).await?; let image = include_bytes!("../assets/welcome-image.jpg"); @@ -464,7 +1464,7 @@ impl Context { chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg)).await?; let mut msg = Message::new(Viewtype::Text); - msg.text = Some(self.stock_str(WelcomeMessage).await.to_string()); + msg.text = Some(WelcomeMessage::stock_str(self).await.to_string()); chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg)).await?; Ok(()) } @@ -498,7 +1498,7 @@ mod tests { t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) .await .unwrap(); - assert_eq!(t.stock_str(StockMessage::NoMessages).await, "xyz") + assert_eq!(NoMessages::stock_str(&t).await, "xyz") } #[async_std::test] @@ -519,37 +1519,22 @@ mod tests { #[async_std::test] async fn test_stock_str() { let t = TestContext::new().await; - assert_eq!(t.stock_str(StockMessage::NoMessages).await, "No messages."); + assert_eq!(NoMessages::stock_str(&t).await, "No messages."); } #[async_std::test] async fn test_stock_string_repl_str() { let t = TestContext::new().await; // uses %1$s substitution - assert_eq!( - t.stock_string_repl_str(StockMessage::MsgAddMember, "Foo") - .await, - "Member Foo added." - ); + assert_eq!(ContactVerified::stock_str(&t, "Foo").await, "Foo verified."); // We have no string using %1$d to test... } - #[async_std::test] - async fn test_stock_string_repl_int() { - let t = TestContext::new().await; - assert_eq!( - t.stock_string_repl_int(StockMessage::MsgAddMember, 42) - .await, - "Member 42 added." - ); - } - #[async_std::test] async fn test_stock_string_repl_str2() { let t = TestContext::new().await; assert_eq!( - t.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar") - .await, + ServerResponse::stock_str(&t, "foo", "bar").await, "Could not connect to foo: bar" ); } @@ -558,8 +1543,7 @@ mod tests { async fn test_stock_system_msg_simple() { let t = TestContext::new().await; assert_eq!( - t.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0) - .await, + MsgLocationEnabled::stock_str(&t).await, "Location streaming enabled." ) } @@ -568,13 +1552,7 @@ mod tests { async fn test_stock_system_msg_add_member_by_me() { let t = TestContext::new().await; assert_eq!( - t.stock_system_msg( - StockMessage::MsgAddMember, - "alice@example.com", - "", - DC_CONTACT_ID_SELF - ) - .await, + MsgAddMember::stock_str(&t, "alice@example.com", DC_CONTACT_ID_SELF).await, "Member alice@example.com added by me." ) } @@ -586,13 +1564,7 @@ mod tests { .await .expect("failed to create contact"); assert_eq!( - t.stock_system_msg( - StockMessage::MsgAddMember, - "alice@example.com", - "", - DC_CONTACT_ID_SELF - ) - .await, + MsgAddMember::stock_str(&t, "alice@example.com", DC_CONTACT_ID_SELF).await, "Member Alice (alice@example.com) added by me." ); } @@ -609,46 +1581,11 @@ mod tests { .expect("failed to create bob") }; assert_eq!( - t.stock_system_msg( - StockMessage::MsgAddMember, - "alice@example.com", - "", - contact_id, - ) - .await, + MsgAddMember::stock_str(&t, "alice@example.com", contact_id,).await, "Member Alice (alice@example.com) added by Bob (bob@example.com)." ); } - #[async_std::test] - async fn test_stock_system_msg_grp_name() { - let t = TestContext::new().await; - assert_eq!( - t.stock_system_msg( - StockMessage::MsgGrpName, - "Some chat", - "Other chat", - DC_CONTACT_ID_SELF - ) - .await, - "Group name changed from \"Some chat\" to \"Other chat\" by me." - ) - } - - #[async_std::test] - async fn test_stock_system_msg_grp_name_other() { - let t = TestContext::new().await; - let id = Contact::create(&t, "Alice", "alice@example.com") - .await - .expect("failed to create contact"); - - assert_eq!( - t.stock_system_msg(StockMessage::MsgGrpName, "Some chat", "Other chat", id) - .await, - "Group name changed from \"Some chat\" to \"Other chat\" by Alice (alice@example.com)." - ) - } - #[async_std::test] async fn test_update_device_chats() { let t = TestContext::new().await;