Compare commits

..

7 Commits

Author SHA1 Message Date
Septias
c68e25a970 fix tests 2024-11-22 10:26:27 +01:00
Septias
e04c4446d6 fix: Disable setting draft if not in chat 2024-11-20 12:52:33 +01:00
Septias
1b9f3368fa fix: only show draft in message info if self is in chat
This PR adds a check to `get_info` so that `draft` is only added if self
is part of the group determined by `can_send`.

close #6229
2024-11-20 12:28:03 +01:00
gerryfrancis
3b9e6d6ffa Fix type in context.rs 2024-11-19 18:35:42 +01:00
Sebastian Klähn
8f3be764d2 change: Use i.delta.chat in qr codes (#6223)
As discussed in #5467 we want to use `i.delta.chat` in QR codes in favor
of `OPENPGP4FPR:` scheme. This PR does the replacement in
`get_securejoin_qr` which is used in `get_securejoin_qr_svg`.

close #5467
2024-11-19 17:32:42 +01:00
Hocuri
c181db631f feat: Clear config cache in start_io() (#6228)
This is needed for iOS (https://github.com/deltachat/deltachat-ios/pull/2393), see comment in the code. An alternative would be
to add an API `invalidate_config_cache()` or to do nothing and just
assume that things will be fine.
2024-11-19 15:59:05 +00:00
link2xt
c18a476806 refactor: forbid clippy::string_slice 2024-11-18 23:57:57 +00:00
12 changed files with 175 additions and 39 deletions

View File

@@ -16,6 +16,7 @@
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,

View File

@@ -1,5 +1,6 @@
#![recursion_limit = "256"]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
pub mod api;
pub use yerpc;

View File

@@ -9,6 +9,7 @@
//!
//! For received messages, DelSp parameter is honoured.
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
/// Wraps line to 72 characters using format=flowed soft breaks.
///

View File

@@ -705,7 +705,7 @@ class TestOfflineChat:
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
qr = ac1.get_setup_contact_qr()
assert qr.startswith("OPENPGP4FPR:")
assert qr.startswith("https://i.delta.chat")
res = ac2.check_qr(qr)
assert res.is_ask_verifycontact()
assert not res.is_ask_verifygroup()

View File

@@ -1,11 +1,11 @@
//! # Chat module.
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use std::{cmp, convert};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
@@ -809,6 +809,14 @@ impl ChatId {
///
/// Passing `None` as message just deletes the draft
pub async fn set_draft(self, context: &Context, mut msg: Option<&mut Message>) -> Result<()> {
let self_in_chat = self.is_self_in_chat(context).await?;
if !self_in_chat {
warn!(
context,
"Not setting draft because self is not member of the chat"
);
return Ok(());
}
if self.is_special() {
return Ok(());
}
@@ -875,6 +883,22 @@ impl ChatId {
> 0)
}
async fn get_chat_type(self, context: &Context) -> Result<Chattype> {
context
.sql
.query_get_value("SELECT type FROM chats WHERE id=?;", (self,))
.await
.map(|res| res.ok_or(anyhow!("Can't get type for char")))
.and_then(convert::identity)
}
async fn is_self_in_chat(self, context: &Context) -> Result<bool> {
match self.get_chat_type(context).await? {
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self, ContactId::SELF).await,
}
}
/// Set provided message as draft message for specified chat.
/// Returns true if the draft was added or updated in place.
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
@@ -1838,8 +1862,9 @@ impl Chat {
/// This is somewhat experimental, even more so than the rest of
/// deltachat, and the data returned is still subject to change.
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
let self_in_chat = self.is_self_in_chat(context).await?;
let draft = match self.id.get_draft(context).await? {
Some(message) => message.text,
Some(message) if self_in_chat => message.text,
_ => String::new(),
};
Ok(ChatInfo {
@@ -4894,6 +4919,70 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_draft_only_in_accesible_chat_summary() -> Result<()> {
let mut t = TestContextManager::new();
let alice = t.alice().await;
let bob = t.bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "chat").await?;
let bob_contact = Contact::create(&alice, "Bob", "bob@example.net").await?;
add_contact_to_chat(&alice, chat_id, bob_contact).await?;
let bob_chat_id = bob
.recv_msg(&alice.send_text(chat_id, "hello bob").await)
.await
.chat_id;
bob_chat_id.accept(&bob).await?;
// Set draft and assure it is in chat info.
let draft = String::from("I'm gonna send this!!");
bob_chat_id
.set_draft(&bob, Some(&mut Message::new_text(draft.clone())))
.await?;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, draft);
// Alice removes bob, so draft is not shown in chat info.
remove_contact_from_chat(&alice, chat_id, bob_contact).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert!(!chat.can_send(&bob).await?);
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, String::from(""));
// Alice re-adds bob, so draft is shown again.
add_contact_to_chat(&alice, chat_id, bob_contact).await?;
let bob_chat_id = bob
.recv_msg(&alice.send_text(chat_id, "hello again, bob").await)
.await
.chat_id;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert!(chat.can_send(&bob).await?);
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, draft);
// Bob leaves group so draft is not shown.
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert!(!chat.can_send(&bob).await?);
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, String::from(""));
// Bob can not set a draft if not in chat.
bob_chat_id
.set_draft(
&bob,
Some(&mut Message::new_text(String::from(
"I'm gonna send this fr fr!!",
))),
)
.await?;
assert_eq!(bob_chat_id.get_draft(&bob).await?.unwrap().text, draft);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
let t = TestContext::new_alice().await;

View File

@@ -472,6 +472,14 @@ impl Context {
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
// until the notifications process finishes.
// Now, some configs may have changed, so, we need to invalidate the cache.
self.sql.config_cache.write().await.clear();
self.scheduler.start(self.clone()).await;
}
@@ -2064,4 +2072,41 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
// because it runs in a different process
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Starting IO will fail of course because no server settings are configured,
// but it should invalidate the caches:
alice.start_io().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("0".to_string())
);
Ok(())
}
}

View File

@@ -382,9 +382,9 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
/// Creates new 160-bit (20 bytes) or 256-bit (32 bytes) fingerprint.
/// Creates new 160-bit (20 bytes) fingerprint.
pub fn new(v: Vec<u8>) -> Fingerprint {
debug_assert!(v.len() == 20 || v.len() == 32);
debug_assert_eq!(v.len(), 20);
Fingerprint(v)
}
@@ -438,11 +438,7 @@ impl std::str::FromStr for Fingerprint {
.filter(|&c| c.is_ascii_hexdigit())
.collect();
let v: Vec<u8> = hex::decode(&hex_repr)?;
ensure!(
v.len() == 20 || v.len() == 32,
"wrong fingerprint length: {}",
hex_repr
);
ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr);
let fp = Fingerprint::new(v);
Ok(fp)
}

View File

@@ -17,6 +17,7 @@
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,

View File

@@ -11,6 +11,7 @@ use pgp::composed::{
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
};
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
@@ -186,12 +187,14 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
let (signing_key_type, encryption_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::Ed25519, PgpKeyType::X25519),
KeyGenType::Ed25519 | KeyGenType::Default => (
PgpKeyType::EdDSALegacy,
PgpKeyType::ECDH(ECCCurve::Curve25519),
),
};
let user_id = format!("<{addr}>");
let key_params = SecretKeyParamsBuilder::default()
.version(pgp::types::KeyVersion::V6)
.key_type(signing_key_type)
.can_certify(true)
.can_sign(true)
@@ -215,7 +218,6 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
])
.subkey(
SubkeyParamsBuilder::default()
.version(pgp::types::KeyVersion::V6)
.key_type(encryption_key_type)
.can_encrypt(true)
.passphrase(None)

View File

@@ -807,11 +807,7 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
.get(MAILTO_SCHEME.len()..)
.context("Invalid mailto: scheme")?;
let (addr, query) = if let Some(query_index) = payload.find('?') {
(&payload[..query_index], &payload[query_index + 1..])
} else {
(payload, "")
};
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
let param: BTreeMap<&str, &str> = query
.split('&')
@@ -861,12 +857,9 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
let addr = if let Some(query_index) = payload.find(':') {
&payload[..query_index]
} else {
bail!("Invalid SMTP found");
};
let (addr, _rest) = payload
.split_once(':')
.context("Invalid SMTP scheme payload")?;
let addr = normalize_address(addr)?;
let name = "";
Qr::from_address(context, name, &addr, None).await

View File

@@ -104,7 +104,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
context.scheduler.interrupt_inbox().await;
}
format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
@@ -119,7 +119,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
context.scheduler.interrupt_inbox().await;
}
format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
"https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
self_name_urlencoded,
@@ -347,7 +347,7 @@ pub(crate) async fn handle_securejoin_handshake(
send_alice_handshake_msg(
context,
contact_id,
&format!("{}-auth-required", &step[..2]),
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
)
.await
.context("failed sending auth-required handshake message")?;

View File

@@ -47,20 +47,27 @@ use crate::stock_str;
/// end of the shortened string.
pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let count = buf.chars().count();
if count > approx_chars + DC_ELLIPSIS.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
.map(|(n, _)| n)
.unwrap_or_default();
if count <= approx_chars + DC_ELLIPSIS.len() {
return Cow::Borrowed(buf);
}
let end_pos = buf
.char_indices()
.nth(approx_chars)
.map(|(n, _)| n)
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind([' ', '\n']) {
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
}
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
Cow::Owned(format!(
"{}{}",
&buf.get(..=index).unwrap_or_default(),
DC_ELLIPSIS
))
} else {
Cow::Borrowed(buf)
Cow::Owned(format!(
"{}{}",
&buf.get(..end_pos).unwrap_or_default(),
DC_ELLIPSIS
))
}
}