use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{bail, Result};
use humansize::{format_size, BINARY};
use strum::EnumProperty as EnumPropertyTrait;
use strum_macros::EnumProperty;
use tokio::sync::RwLock;
use crate::accounts::Accounts;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::tools::timestamp_to_str;
#[derive(Debug, Clone)]
pub struct StockStrings {
translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
#[repr(u32)]
pub enum StockMessage {
#[strum(props(fallback = "No messages."))]
NoMessages = 1,
#[strum(props(fallback = "Me"))]
SelfMsg = 2,
#[strum(props(fallback = "Draft"))]
Draft = 3,
#[strum(props(fallback = "Voice message"))]
VoiceMessage = 7,
#[strum(props(fallback = "Image"))]
Image = 9,
#[strum(props(fallback = "Video"))]
Video = 10,
#[strum(props(fallback = "Audio"))]
Audio = 11,
#[strum(props(fallback = "File"))]
File = 12,
#[strum(props(fallback = "GIF"))]
Gif = 23,
#[strum(props(fallback = "Encrypted message"))]
EncryptedMsg = 24,
#[strum(props(fallback = "End-to-end encryption available"))]
E2eAvailable = 25,
#[strum(props(fallback = "No encryption"))]
EncrNone = 28,
#[strum(props(fallback = "This message was encrypted for another setup."))]
CantDecryptMsgBody = 29,
#[strum(props(fallback = "Fingerprints"))]
FingerPrints = 30,
#[strum(props(fallback = "End-to-end encryption preferred"))]
E2ePreferred = 34,
#[strum(props(fallback = "%1$s verified."))]
ContactVerified = 35,
#[strum(props(fallback = "Cannot establish guaranteed end-to-end encryption with %1$s"))]
ContactNotVerified = 36,
#[strum(props(fallback = "Changed setup for %1$s"))]
ContactSetupChanged = 37,
#[strum(props(fallback = "Archived chats"))]
ArchivedChats = 40,
#[strum(props(fallback = "Autocrypt Setup Message"))]
AcSetupMsgSubject = 42,
#[strum(props(
fallback = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\nTo decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."
))]
AcSetupMsgBody = 43,
#[strum(props(
fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
))]
CannotLogin = 60,
#[strum(props(fallback = "Location streaming enabled."))]
MsgLocationEnabled = 64,
#[strum(props(fallback = "Location streaming disabled."))]
MsgLocationDisabled = 65,
#[strum(props(fallback = "Location"))]
Location = 66,
#[strum(props(fallback = "Sticker"))]
Sticker = 67,
#[strum(props(fallback = "Device messages"))]
DeviceMessages = 68,
#[strum(props(fallback = "Saved messages"))]
SavedMessages = 69,
#[strum(props(
fallback = "Messages in this chat are generated locally by your Delta Chat app. \
Its makers use it to inform about app updates and problems during usage."
))]
DeviceMessagesHint = 70,
#[strum(props(fallback = "Welcome to Delta Chat! – \
Delta Chat looks and feels like other popular messenger apps, \
but does not involve centralized control, \
tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
Technically, Delta Chat is an email application with a modern chat interface. \
Email in a new dress if you will 👻\n\n\
Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown sender for this chat."))]
UnknownSenderForChat = 72,
#[strum(props(fallback = "Message from %1$s"))]
SubjectForNewContact = 73,
#[strum(props(fallback = "Failed to send message to %1$s."))]
FailedSendingTo = 74,
#[strum(props(fallback = "Video chat invitation"))]
VideochatInvitation = 82,
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
VideochatInviteMsgBody = 83,
#[strum(props(fallback = "Error:\n\n“%1$s”"))]
ConfigurationFailed = 84,
#[strum(props(
fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
Adjust your clock ⏰🔧 to ensure your messages are received correctly."
))]
BadTimeMsgBody = 85,
#[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
This may cause problems because your chat partners use newer versions - \
and you are missing the latest features 😳\n\
Please check https://get.delta.chat or your app store for updates."))]
UpdateReminderMsgBody = 86,
#[strum(props(
fallback = "Could not find your mail server.\n\nPlease check your internet connection."
))]
ErrorNoNetwork = 87,
#[strum(props(fallback = "Reply"))]
ReplyNoun = 90,
#[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
To use the \"Saved messages\" feature again, create a new chat with yourself."))]
SelfDeletedMsgBody = 91,
#[strum(props(
fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
))]
DeleteServerTurnedOff = 92,
#[strum(props(fallback = "Forwarded"))]
Forwarded = 97,
#[strum(props(
fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
You may not be able to receive message when the storage is 100%% used.\n\n\
👉 Please check if you can delete old data in the provider's webinterface \
and consider to enable \"Settings / Delete Old Messages\". \
You can check your current storage usage anytime at \"Settings / Connectivity\"."
))]
QuotaExceedingMsgBody = 98,
#[strum(props(fallback = "%1$s message"))]
PartialDownloadMsgBody = 99,
#[strum(props(fallback = "Download maximum available until %1$s"))]
DownloadAvailability = 100,
#[strum(props(fallback = "Multi Device Synchronization"))]
SyncMsgSubject = 101,
#[strum(props(
fallback = "This message is used to synchronize data between your devices.\n\n\
👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
))]
SyncMsgBody = 102,
#[strum(props(fallback = "Incoming Messages"))]
IncomingMessages = 103,
#[strum(props(fallback = "Outgoing Messages"))]
OutgoingMessages = 104,
#[strum(props(fallback = "Storage on %1$s"))]
StorageOnDomain = 105,
#[strum(props(fallback = "Connected"))]
Connected = 107,
#[strum(props(fallback = "Connecting…"))]
Connecting = 108,
#[strum(props(fallback = "Updating…"))]
Updating = 109,
#[strum(props(fallback = "Sending…"))]
Sending = 110,
#[strum(props(fallback = "Your last message was sent successfully."))]
LastMsgSentSuccessfully = 111,
#[strum(props(fallback = "Error: %1$s"))]
Error = 112,
#[strum(props(fallback = "Not supported by your provider."))]
NotSupportedByProvider = 113,
#[strum(props(fallback = "Messages"))]
Messages = 114,
#[strum(props(fallback = "Broadcast List"))]
BroadcastList = 115,
#[strum(props(fallback = "%1$s of %2$s used"))]
PartOfTotallUsed = 116,
#[strum(props(fallback = "%1$s invited you to join this group.\n\n\
Waiting for the device of %2$s to reply…"))]
SecureJoinStarted = 117,
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
SecureJoinReplies = 118,
#[strum(props(fallback = "Scan to chat with %1$s"))]
SetupContactQRDescription = 119,
#[strum(props(fallback = "Scan to join group %1$s"))]
SecureJoinGroupQRDescription = 120,
#[strum(props(fallback = "Not connected"))]
NotConnected = 121,
#[strum(props(fallback = "%1$s changed their address from %2$s to %3$s"))]
AeapAddrChanged = 122,
#[strum(props(
fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
))]
AeapExplanationAndLink = 123,
#[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
MsgYouChangedGrpName = 124,
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
MsgGrpNameChangedBy = 125,
#[strum(props(fallback = "You changed the group image."))]
MsgYouChangedGrpImg = 126,
#[strum(props(fallback = "Group image changed by %1$s."))]
MsgGrpImgChangedBy = 127,
#[strum(props(fallback = "You added member %1$s."))]
MsgYouAddMember = 128,
#[strum(props(fallback = "Member %1$s added by %2$s."))]
MsgAddMemberBy = 129,
#[strum(props(fallback = "You removed member %1$s."))]
MsgYouDelMember = 130,
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
MsgDelMemberBy = 131,
#[strum(props(fallback = "You left the group."))]
MsgYouLeftGroup = 132,
#[strum(props(fallback = "Group left by %1$s."))]
MsgGroupLeftBy = 133,
#[strum(props(fallback = "You deleted the group image."))]
MsgYouDeletedGrpImg = 134,
#[strum(props(fallback = "Group image deleted by %1$s."))]
MsgGrpImgDeletedBy = 135,
#[strum(props(fallback = "You enabled location streaming."))]
MsgYouEnabledLocation = 136,
#[strum(props(fallback = "Location streaming enabled by %1$s."))]
MsgLocationEnabledBy = 137,
#[strum(props(fallback = "You disabled message deletion timer."))]
MsgYouDisabledEphemeralTimer = 138,
#[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
MsgEphemeralTimerDisabledBy = 139,
#[strum(props(fallback = "You set message deletion timer to %1$s s."))]
MsgYouEnabledEphemeralTimer = 140,
#[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
MsgEphemeralTimerEnabledBy = 141,
#[strum(props(fallback = "You set message deletion timer to 1 minute."))]
MsgYouEphemeralTimerMinute = 142,
#[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
MsgEphemeralTimerMinuteBy = 143,
#[strum(props(fallback = "You set message deletion timer to 1 hour."))]
MsgYouEphemeralTimerHour = 144,
#[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
MsgEphemeralTimerHourBy = 145,
#[strum(props(fallback = "You set message deletion timer to 1 day."))]
MsgYouEphemeralTimerDay = 146,
#[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
MsgEphemeralTimerDayBy = 147,
#[strum(props(fallback = "You set message deletion timer to 1 week."))]
MsgYouEphemeralTimerWeek = 148,
#[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
MsgEphemeralTimerWeekBy = 149,
#[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
MsgYouEphemeralTimerMinutes = 150,
#[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
MsgEphemeralTimerMinutesBy = 151,
#[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
MsgYouEphemeralTimerHours = 152,
#[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
MsgEphemeralTimerHoursBy = 153,
#[strum(props(fallback = "You set message deletion timer to %1$s days."))]
MsgYouEphemeralTimerDays = 154,
#[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
MsgEphemeralTimerDaysBy = 155,
#[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
MsgYouEphemeralTimerWeeks = 156,
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
MsgEphemeralTimerWeeksBy = 157,
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
BackupTransferQr = 162,
#[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
BackupTransferMsgBody = 163,
#[strum(props(fallback = "I added member %1$s."))]
MsgIAddMember = 164,
#[strum(props(fallback = "I removed member %1$s."))]
MsgIDelMember = 165,
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
ChatProtectionEnabled = 170,
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
NewGroupSendFirstMessage = 172,
#[strum(props(fallback = "Member %1$s added."))]
MsgAddMember = 173,
#[strum(props(
fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
))]
InvalidUnencryptedMail = 174,
#[strum(props(
fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
))]
CantDecryptOutgoingMsgs = 175,
#[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
MsgYouReacted = 176,
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
MsgReactedBy = 177,
#[strum(props(fallback = "Member %1$s removed."))]
MsgDelMember = 178,
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
SecurejoinWait = 190,
#[strum(props(
fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
))]
SecurejoinWaitTimeout = 191,
}
impl StockMessage {
fn fallback(self) -> &'static str {
self.get_str("fallback").unwrap_or_default()
}
}
impl Default for StockStrings {
fn default() -> Self {
StockStrings::new()
}
}
impl StockStrings {
pub fn new() -> Self {
Self {
translated_stockstrings: Arc::new(RwLock::new(Default::default())),
}
}
async fn translated(&self, id: StockMessage) -> String {
self.translated_stockstrings
.read()
.await
.get(&(id as usize))
.map(AsRef::as_ref)
.unwrap_or_else(|| id.fallback())
.to_string()
}
async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
if stockstring.contains("%1") && !id.fallback().contains("%1") {
bail!(
"translation {} contains invalid %1 placeholder, default is {}",
stockstring,
id.fallback()
);
}
if stockstring.contains("%2") && !id.fallback().contains("%2") {
bail!(
"translation {} contains invalid %2 placeholder, default is {}",
stockstring,
id.fallback()
);
}
self.translated_stockstrings
.write()
.await
.insert(id as usize, stockstring);
Ok(())
}
}
async fn translated(context: &Context, id: StockMessage) -> String {
context.translated_stockstrings.translated(id).await
}
trait StockStringMods: AsRef<str> + Sized {
fn replace1(&self, replacement: &str) -> String {
self.as_ref()
.replacen("%1$s", replacement, 1)
.replacen("%1$d", replacement, 1)
.replacen("%1$@", replacement, 1)
}
fn replace2(&self, replacement: &str) -> String {
self.as_ref()
.replacen("%2$s", replacement, 1)
.replacen("%2$d", replacement, 1)
.replacen("%2$@", replacement, 1)
}
fn replace3(&self, replacement: &str) -> String {
self.as_ref()
.replacen("%3$s", replacement, 1)
.replacen("%3$d", replacement, 1)
.replacen("%3$@", replacement, 1)
}
}
impl ContactId {
async fn get_stock_name_n_addr(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_else(|_| self.to_string())
}
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_display_name().to_string())
.unwrap_or_else(|_| self.to_string())
}
}
impl StockStringMods for String {}
pub(crate) async fn no_messages(context: &Context) -> String {
translated(context, StockMessage::NoMessages).await
}
pub(crate) async fn self_msg(context: &Context) -> String {
translated(context, StockMessage::SelfMsg).await
}
pub(crate) async fn draft(context: &Context) -> String {
translated(context, StockMessage::Draft).await
}
pub(crate) async fn voice_message(context: &Context) -> String {
translated(context, StockMessage::VoiceMessage).await
}
pub(crate) async fn image(context: &Context) -> String {
translated(context, StockMessage::Image).await
}
pub(crate) async fn video(context: &Context) -> String {
translated(context, StockMessage::Video).await
}
pub(crate) async fn audio(context: &Context) -> String {
translated(context, StockMessage::Audio).await
}
pub(crate) async fn file(context: &Context) -> String {
translated(context, StockMessage::File).await
}
pub(crate) async fn msg_grp_name(
context: &Context,
from_group: &str,
to_group: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouChangedGrpName)
.await
.replace1(from_group)
.replace2(to_group)
} else {
translated(context, StockMessage::MsgGrpNameChangedBy)
.await
.replace1(from_group)
.replace2(to_group)
.replace3(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouChangedGrpImg).await
} else {
translated(context, StockMessage::MsgGrpImgChangedBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
let addr = added_member_addr;
let whom = &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_authname_n_addr())
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
translated(context, StockMessage::MsgIAddMember)
.await
.replace1(whom)
}
pub(crate) async fn msg_add_member_local(
context: &Context,
added_member_addr: &str,
by_contact: ContactId,
) -> String {
let addr = added_member_addr;
let whom = &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(),
};
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgAddMember)
.await
.replace1(whom)
} else if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouAddMember)
.await
.replace1(whom)
} else {
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
let addr = removed_member_addr;
let whom = &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_authname_n_addr())
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
translated(context, StockMessage::MsgIDelMember)
.await
.replace1(whom)
}
pub(crate) async fn msg_del_member_local(
context: &Context,
removed_member_addr: &str,
by_contact: ContactId,
) -> String {
let addr = removed_member_addr;
let whom = &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(),
};
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgDelMember)
.await
.replace1(whom)
} else if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDelMember)
.await
.replace1(whom)
} else {
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
translated(context, StockMessage::MsgILeftGroup).await
}
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouLeftGroup).await
} else {
translated(context, StockMessage::MsgGroupLeftBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_reacted(
context: &Context,
by_contact: ContactId,
reaction: &str,
summary: &str,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouReacted)
.await
.replace1(reaction)
.replace2(summary)
} else {
translated(context, StockMessage::MsgReactedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace2(reaction)
.replace3(summary)
}
}
pub(crate) async fn gif(context: &Context) -> String {
translated(context, StockMessage::Gif).await
}
pub(crate) async fn e2e_available(context: &Context) -> String {
translated(context, StockMessage::E2eAvailable).await
}
pub(crate) async fn encr_none(context: &Context) -> String {
translated(context, StockMessage::EncrNone).await
}
pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
translated(context, StockMessage::CantDecryptMsgBody).await
}
pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String {
translated(context, StockMessage::CantDecryptOutgoingMsgs).await
}
pub(crate) async fn finger_prints(context: &Context) -> String {
translated(context, StockMessage::FingerPrints).await
}
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDeletedGrpImg).await
} else {
translated(context, StockMessage::MsgGrpImgDeletedBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn e2e_preferred(context: &Context) -> String {
translated(context, StockMessage::E2ePreferred).await
}
pub(crate) async fn secure_join_started(
context: &Context,
inviter_contact_id: ContactId,
) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
.replace1(&contact.get_name_n_addr())
.replace2(contact.get_display_name())
} else {
format!("secure_join_started: unknown contact {inviter_contact_id}")
}
}
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
pub(crate) async fn securejoin_wait(context: &Context) -> String {
translated(context, StockMessage::SecurejoinWait).await
}
pub(crate) async fn securejoin_wait_timeout(context: &Context) -> String {
translated(context, StockMessage::SecurejoinWaitTimeout).await
}
pub(crate) async fn setup_contact_qr_description(
context: &Context,
display_name: &str,
addr: &str,
) -> String {
let name = if display_name.is_empty() {
addr.to_owned()
} else {
display_name.to_owned()
};
translated(context, StockMessage::SetupContactQRDescription)
.await
.replace1(&name)
}
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
translated(context, StockMessage::SecureJoinGroupQRDescription)
.await
.replace1(chat.get_name())
}
#[allow(dead_code)]
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr();
translated(context, StockMessage::ContactVerified)
.await
.replace1(addr)
}
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr();
translated(context, StockMessage::ContactNotVerified)
.await
.replace1(addr)
}
pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String {
translated(context, StockMessage::ContactSetupChanged)
.await
.replace1(contact_addr)
}
pub(crate) async fn archived_chats(context: &Context) -> String {
translated(context, StockMessage::ArchivedChats).await
}
pub(crate) async fn ac_setup_msg_subject(context: &Context) -> String {
translated(context, StockMessage::AcSetupMsgSubject).await
}
pub(crate) async fn ac_setup_msg_body(context: &Context) -> String {
translated(context, StockMessage::AcSetupMsgBody).await
}
pub(crate) async fn sync_msg_subject(context: &Context) -> String {
translated(context, StockMessage::SyncMsgSubject).await
}
pub(crate) async fn sync_msg_body(context: &Context) -> String {
translated(context, StockMessage::SyncMsgBody).await
}
pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
translated(context, StockMessage::CannotLogin)
.await
.replace1(user)
}
pub(crate) async fn msg_location_enabled(context: &Context) -> String {
translated(context, StockMessage::MsgLocationEnabled).await
}
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
if contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEnabledLocation).await
} else {
translated(context, StockMessage::MsgLocationEnabledBy)
.await
.replace1(&contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_location_disabled(context: &Context) -> String {
translated(context, StockMessage::MsgLocationDisabled).await
}
pub(crate) async fn location(context: &Context) -> String {
translated(context, StockMessage::Location).await
}
pub(crate) async fn sticker(context: &Context) -> String {
translated(context, StockMessage::Sticker).await
}
pub(crate) async fn device_messages(context: &Context) -> String {
translated(context, StockMessage::DeviceMessages).await
}
pub(crate) async fn saved_messages(context: &Context) -> String {
translated(context, StockMessage::SavedMessages).await
}
pub(crate) async fn device_messages_hint(context: &Context) -> String {
translated(context, StockMessage::DeviceMessagesHint).await
}
pub(crate) async fn welcome_message(context: &Context) -> String {
translated(context, StockMessage::WelcomeMessage).await
}
pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
translated(context, StockMessage::UnknownSenderForChat).await
}
pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
translated(context, StockMessage::SubjectForNewContact)
.await
.replace1(self_name)
}
pub(crate) async fn msg_ephemeral_timer_disabled(
context: &Context,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
} else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_enabled(
context: &Context,
timer: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
.await
.replace1(timer)
} else {
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await
.replace1(timer)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
} else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerHour).await
} else {
translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerDay).await
} else {
translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
} else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn videochat_invitation(context: &Context) -> String {
translated(context, StockMessage::VideochatInvitation).await
}
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
translated(context, StockMessage::VideochatInviteMsgBody)
.await
.replace1(url)
}
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
translated(context, StockMessage::ConfigurationFailed)
.await
.replace1(details)
}
pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
translated(context, StockMessage::BadTimeMsgBody)
.await
.replace1(now)
}
pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
translated(context, StockMessage::UpdateReminderMsgBody).await
}
pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await
}
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
}
pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::ChatProtectionDisabled)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
pub(crate) async fn reply_noun(context: &Context) -> String {
translated(context, StockMessage::ReplyNoun).await
}
pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
translated(context, StockMessage::SelfDeletedMsgBody).await
}
pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
translated(context, StockMessage::DeleteServerTurnedOff).await
}
pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context,
minutes: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
.await
.replace1(minutes)
} else {
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await
.replace1(minutes)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_hours(
context: &Context,
hours: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerHours)
.await
.replace1(hours)
} else {
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await
.replace1(hours)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_days(
context: &Context,
days: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerDays)
.await
.replace1(days)
} else {
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await
.replace1(days)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn msg_ephemeral_timer_weeks(
context: &Context,
weeks: &str,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
.await
.replace1(weeks)
} else {
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await
.replace1(weeks)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
}
}
pub(crate) async fn forwarded(context: &Context) -> String {
translated(context, StockMessage::Forwarded).await
}
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
translated(context, StockMessage::QuotaExceedingMsgBody)
.await
.replace1(&format!("{highest_usage}"))
.replace("%%", "%")
}
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
let size = &format_size(org_bytes, BINARY);
translated(context, StockMessage::PartialDownloadMsgBody)
.await
.replace1(size)
}
pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
translated(context, StockMessage::DownloadAvailability)
.await
.replace1(×tamp_to_str(timestamp))
}
pub(crate) async fn incoming_messages(context: &Context) -> String {
translated(context, StockMessage::IncomingMessages).await
}
pub(crate) async fn outgoing_messages(context: &Context) -> String {
translated(context, StockMessage::OutgoingMessages).await
}
pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
translated(context, StockMessage::StorageOnDomain)
.await
.replace1(domain)
}
pub(crate) async fn not_connected(context: &Context) -> String {
translated(context, StockMessage::NotConnected).await
}
pub(crate) async fn connected(context: &Context) -> String {
translated(context, StockMessage::Connected).await
}
pub(crate) async fn connecting(context: &Context) -> String {
translated(context, StockMessage::Connecting).await
}
pub(crate) async fn updating(context: &Context) -> String {
translated(context, StockMessage::Updating).await
}
pub(crate) async fn sending(context: &Context) -> String {
translated(context, StockMessage::Sending).await
}
pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
translated(context, StockMessage::LastMsgSentSuccessfully).await
}
pub(crate) async fn error(context: &Context, error: &str) -> String {
translated(context, StockMessage::Error)
.await
.replace1(error)
}
pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
translated(context, StockMessage::NotSupportedByProvider).await
}
pub(crate) async fn messages(context: &Context) -> String {
translated(context, StockMessage::Messages).await
}
pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
translated(context, StockMessage::PartOfTotallUsed)
.await
.replace1(part)
.replace2(total)
}
pub(crate) async fn broadcast_list(context: &Context) -> String {
translated(context, StockMessage::BroadcastList).await
}
pub(crate) async fn aeap_addr_changed(
context: &Context,
contact_name: &str,
old_addr: &str,
new_addr: &str,
) -> String {
translated(context, StockMessage::AeapAddrChanged)
.await
.replace1(contact_name)
.replace2(old_addr)
.replace3(new_addr)
}
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
translated(context, StockMessage::InvalidUnencryptedMail)
.await
.replace1(provider)
}
pub(crate) async fn aeap_explanation_and_link(
context: &Context,
old_addr: &str,
new_addr: &str,
) -> String {
translated(context, StockMessage::AeapExplanationAndLink)
.await
.replace1(old_addr)
.replace2(new_addr)
}
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
translated(context, StockMessage::NewGroupSendFirstMessage).await
}
pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
let contact = Contact::get_by_id(context, ContactId::SELF).await?;
let addr = contact.get_addr();
let full_name = match context.get_config(Config::Displayname).await? {
Some(name) if name != addr => format!("{name} ({addr})"),
_ => addr.to_string(),
};
Ok(translated(context, StockMessage::BackupTransferQr)
.await
.replace1(&full_name))
}
pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
translated(context, StockMessage::BackupTransferMsgBody).await
}
impl Context {
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
self.translated_stockstrings
.set_stock_translation(id, stockstring)
.await?;
Ok(())
}
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
contact_id: Option<ContactId>,
) -> String {
match protect {
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {
"[Error] No contact_id given".to_string()
}
}
ProtectionStatus::Protected => chat_protection_enabled(self).await,
}
}
pub(crate) async fn update_device_chats(&self) -> Result<()> {
if self.get_config_bool(Config::Bot).await? {
return Ok(());
}
if !self.sql.get_raw_config_bool("self-chat-added").await? {
self.sql
.set_raw_config_bool("self-chat-added", true)
.await?;
ChatId::create_for_contact(self, ContactId::SELF).await?;
}
let image = include_bytes!("../assets/welcome-image.jpg");
let blob = BlobObject::create(self, "welcome-image.jpg", image).await?;
let mut msg = Message::new(Viewtype::Image);
msg.param.set(Param::File, blob.as_name());
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
let mut msg = Message::new_text(welcome_message(self).await);
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
Ok(())
}
}
impl Accounts {
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
self.stockstrings
.set_stock_translation(id, stockstring)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use num_traits::ToPrimitive;
use super::*;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
#[test]
fn test_enum_mapping() {
assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1);
assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2);
}
#[test]
fn test_fallback() {
assert_eq!(StockMessage::NoMessages.fallback(), "No messages.");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_stock_translation() {
let t = TestContext::new().await;
t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
.await
.unwrap();
assert_eq!(no_messages(&t).await, "xyz")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_stock_translation_wrong_replacements() {
let t = TestContext::new().await;
assert!(t
.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
.await
.is_err());
assert!(t
.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
.await
.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_str() {
let t = TestContext::new().await;
assert_eq!(no_messages(&t).await, "No messages.");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_string_repl_str() {
let t = TestContext::new().await;
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
.await
.unwrap();
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
assert_eq!(
contact_verified(&t, &contact).await,
"Someone (someone@example.org) verified."
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_system_msg_simple() {
let t = TestContext::new().await;
assert_eq!(
msg_location_enabled(&t).await,
"Location streaming enabled."
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_system_msg_add_member_by_me() {
let t = TestContext::new().await;
assert_eq!(
msg_add_member_remote(&t, "alice@example.org").await,
"I added member alice@example.org."
);
assert_eq!(
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
"You added member alice@example.org."
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
let t = TestContext::new().await;
Contact::create(&t, "Alice", "alice@example.org")
.await
.expect("failed to create contact");
assert_eq!(
msg_add_member_remote(&t, "alice@example.org").await,
"I added member alice@example.org."
);
assert_eq!(
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
"You added member Alice (alice@example.org)."
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
let t = TestContext::new().await;
let contact_id = {
Contact::create(&t, "Alice", "alice@example.org")
.await
.expect("Failed to create contact Alice");
Contact::create(&t, "Bob", "bob@example.com")
.await
.expect("failed to create bob")
};
assert_eq!(
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quota_exceeding_stock_str() -> Result<()> {
let t = TestContext::new().await;
let str = quota_exceeding(&t, 81).await;
assert!(str.contains("81% "));
assert!(str.contains("100% "));
assert!(!str.contains("%%"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_msg_body() -> Result<()> {
let t = TestContext::new().await;
let str = partial_download_msg_body(&t, 1024 * 1024).await;
assert_eq!(str, "1 MiB message");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_device_chats() {
let t = TestContext::new().await;
t.update_device_chats().await.ok();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap();
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
} else {
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
};
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len();
self_talk_id.delete(&t).await.ok();
assert_eq!(
chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(),
device_chat_msgs_before + 1
);
device_chat_id.delete(&t).await.ok();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
delete_and_reset_all_device_msgs(&t).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
}
}