mirror of
https://github.com/chatmail/core.git
synced 2026-04-09 09:02:10 +03:00
Compare commits
16 Commits
v2.18.0
...
cli-displa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18044c2fef | ||
|
|
f5dea1d252 | ||
|
|
8dd7c42f69 | ||
|
|
b542eeecc0 | ||
|
|
bee8295daa | ||
|
|
ab9fd3d5ed | ||
|
|
cc54a3feda | ||
|
|
94984f35ec | ||
|
|
0e47e89d63 | ||
|
|
2d7dc7a1be | ||
|
|
4d76a5b599 | ||
|
|
87035ff744 | ||
|
|
e0d123f732 | ||
|
|
8eddcfc9d2 | ||
|
|
af58b86b60 | ||
|
|
00ae7ce33c |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -228,9 +228,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: macos-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -281,11 +281,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: macos-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: windows-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## [2.20.0] - 2025-10-13
|
||||
|
||||
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
|
||||
when trying to delete a message with unknown viewtype.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Accept unknown viewtype in ephemeral loop.
|
||||
- Accept unknown viewtype in delete-old-messages loop.
|
||||
|
||||
## [2.19.0] - 2025-10-12
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Slightly increase saturation of colors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to receive call accepted/ended messages referring to non-call Message-ID.
|
||||
- Do not fail to fully download previously trashed messages.
|
||||
- Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that ([#7296](https://github.com/chatmail/core/pull/7296)).
|
||||
- Do not try to process calls from partial messages.
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Python 3.14.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use variables directly in formatted strings ([#7284](https://github.com/chatmail/core/pull/7284)).
|
||||
- Set_chat_profile_image(): Remove !chat.is_mailing_list() check.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump quick-xml from 0.37.5 to 0.38.3.
|
||||
- Add nodejs to nix dev env ([#7283](https://github.com/chatmail/core/pull/7283))
|
||||
|
||||
## [2.18.0] - 2025-10-08
|
||||
|
||||
### API-Changes
|
||||
@@ -6882,3 +6919,5 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.16.0]: https://github.com/chatmail/core/compare/v2.15.0..v2.16.0
|
||||
[2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0
|
||||
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
|
||||
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
|
||||
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -1289,7 +1289,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1399,7 +1399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1421,7 +1421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1437,7 +1437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1466,7 +1466,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -4639,9 +4639,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
version = "0.38.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
@@ -82,7 +82,7 @@ percent-encoding = "2.3"
|
||||
pgp = { version = "0.17.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
|
||||
@@ -68,7 +68,7 @@ impl ContactAddress {
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
bail!("invalid address {s:?}");
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
@@ -257,16 +257,16 @@ impl EmailAddress {
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
|
||||
}
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
bail!("empty string is not valid for local part in {:?}", input);
|
||||
bail!("empty string is not valid for local part in {input:?}");
|
||||
}
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
bail!("missing domain after '@' in {input:?}");
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
@@ -276,7 +276,7 @@ impl EmailAddress {
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
_ => bail!("Email {input:?} must contain '@' character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -126,7 +126,7 @@ impl CommandApi {
|
||||
.read()
|
||||
.await
|
||||
.get_account(id)
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
@@ -308,8 +308,7 @@ impl CommandApi {
|
||||
Ok(Account::from_context(&ctx, account_id).await?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"account with id {} doesn't exist anymore",
|
||||
account_id
|
||||
"account with id {account_id} doesn't exist anymore"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -2312,8 +2311,7 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
ensure!(
|
||||
message.get_viewtype() == Viewtype::Sticker,
|
||||
"message {} is not a sticker",
|
||||
msg_id
|
||||
"message {msg_id} is not a sticker"
|
||||
);
|
||||
let account_folder = ctx
|
||||
.get_dbfile()
|
||||
@@ -2533,10 +2531,7 @@ impl CommandApi {
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
||||
use deltachat::context::Context;
|
||||
@@ -26,7 +26,9 @@ pub struct JsonrpcCallInfo {
|
||||
|
||||
impl JsonrpcCallInfo {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
|
||||
let call_info = context.load_call_by_id(msg_id).await?;
|
||||
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
|
||||
format!("Attempting to get call state of non-call message {msg_id}")
|
||||
})?;
|
||||
let sdp_offer = call_info.place_call_info.clone();
|
||||
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
|
||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.18.0"
|
||||
"version": "2.20.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -418,7 +418,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Ok(setup_code) => {
|
||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||
}
|
||||
Err(err) => bail!("Failed to generate setup code: {}", err),
|
||||
Err(err) => bail!("Failed to generate setup code: {err}"),
|
||||
},
|
||||
"get-setupcodebegin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
@@ -432,7 +432,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{} is no setup message.", msg_id,);
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
@@ -527,7 +527,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Report written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get connectivity html: {}", err);
|
||||
bail!("Failed to get connectivity html: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1287,7 +1287,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -466,7 +466,7 @@ async fn handle_cmd(
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,6 +19,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -92,6 +92,12 @@ def _run_cli(
|
||||
)
|
||||
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
|
||||
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
|
||||
parser.add_argument(
|
||||
"--displayname", action="store", help="the profile's display name", default=os.getenv("DELTACHAT_DISPLAYNAME"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--avatar", action="store", help="filename of the profile's avatar", default=os.getenv("DELTACHAT_AVATAR"),
|
||||
)
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
@@ -108,7 +114,12 @@ def _run_cli(
|
||||
configure_thread = Thread(
|
||||
target=client.configure,
|
||||
daemon=True,
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
kwargs={
|
||||
"email": args.email,
|
||||
"password": args.password,
|
||||
"displayname": args.displayname,
|
||||
"selfavatar": args.avatar,
|
||||
},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.18.0"
|
||||
"version": "2.20.0"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,22 @@ async fn main_impl() -> Result<()> {
|
||||
if let Some(first_arg) = args.next() {
|
||||
if first_arg.to_str() == Some("--version") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
return Ok(());
|
||||
} else if first_arg.to_str() == Some("--openrpc") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
println!("{}", CommandApi::openrpc_specification()?);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||
return Err(anyhow!("Unrecognized option {first_arg:?}"));
|
||||
}
|
||||
}
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
|
||||
// Install signal handlers early so that the shutdown is graceful starting from here.
|
||||
|
||||
@@ -587,6 +587,7 @@
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.18.0"
|
||||
version = "2.20.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-10-08
|
||||
2025-10-13
|
||||
@@ -78,7 +78,7 @@ impl Accounts {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
@@ -724,8 +724,7 @@ impl Config {
|
||||
{
|
||||
ensure!(
|
||||
self.inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {}",
|
||||
id
|
||||
"invalid account id: {id}"
|
||||
);
|
||||
|
||||
self.inner.selected_account = id;
|
||||
|
||||
@@ -35,7 +35,7 @@ impl FromStr for EncryptPreference {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
_ => bail!("Cannot parse encryption preference {}", s),
|
||||
_ => bail!("Cannot parse encryption preference {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) async fn handle_authres(
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
|
||||
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ impl<'a> BlobObject<'a> {
|
||||
false => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {}", name));
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -458,8 +458,7 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B.",
|
||||
max_bytes,
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
63
src/calls.rs
63
src/calls.rs
@@ -96,7 +96,7 @@ impl CallInfo {
|
||||
let duration = match minutes {
|
||||
0 => "<1 minute".to_string(),
|
||||
1 => "1 minute".to_string(),
|
||||
n => format!("{} minutes", n),
|
||||
n => format!("{n} minutes"),
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
@@ -213,7 +213,9 @@ impl Context {
|
||||
call_id: MsgId,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
|
||||
format!("accept_incoming_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
ensure!(call.is_incoming());
|
||||
if call.is_accepted() || call.is_ended() {
|
||||
info!(self, "Call already accepted/ended");
|
||||
@@ -248,7 +250,9 @@ impl Context {
|
||||
|
||||
/// Cancel, decline or hangup an incoming or outgoing call.
|
||||
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
|
||||
format!("end_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
if call.is_ended() {
|
||||
info!(self, "Call already ended");
|
||||
return Ok(());
|
||||
@@ -291,7 +295,13 @@ impl Context {
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let mut call = context.load_call_by_id(call_id).await?;
|
||||
let Some(mut call) = context.load_call_by_id(call_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
@@ -316,7 +326,10 @@ impl Context {
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
if mime_message.is_call() {
|
||||
let call = self.load_call_by_id(call_id).await?;
|
||||
let Some(call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
@@ -352,7 +365,11 @@ impl Context {
|
||||
} else {
|
||||
match mime_message.is_system_message {
|
||||
SystemMessage::CallAccepted => {
|
||||
let mut call = self.load_call_by_id(call_id).await?;
|
||||
let Some(mut call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_ended() || call.is_accepted() {
|
||||
info!(self, "CallAccepted received for accepted/ended call");
|
||||
return Ok(());
|
||||
@@ -377,7 +394,11 @@ impl Context {
|
||||
}
|
||||
}
|
||||
SystemMessage::CallEnded => {
|
||||
let mut call = self.load_call_by_id(call_id).await?;
|
||||
let Some(mut call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_ended() {
|
||||
// may happen eg. if a a message is missed
|
||||
info!(self, "CallEnded received for ended call");
|
||||
@@ -421,15 +442,26 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Loads information about the call given its ID.
|
||||
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<CallInfo> {
|
||||
///
|
||||
/// If the message referred to by ID is
|
||||
/// not a call message, returns `None`.
|
||||
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
|
||||
let call = Message::load_from_db(self, call_id).await?;
|
||||
self.load_call_by_message(call)
|
||||
Ok(self.load_call_by_message(call))
|
||||
}
|
||||
|
||||
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
|
||||
ensure!(call.viewtype == Viewtype::Call);
|
||||
// Loads information about the call given the `Message`.
|
||||
//
|
||||
// If the `Message` is not a call message, returns `None`
|
||||
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
|
||||
if call.viewtype != Viewtype::Call {
|
||||
// This can happen e.g. if a "call accepted"
|
||||
// or "call ended" message is received
|
||||
// with `In-Reply-To` referring to non-call message.
|
||||
return None;
|
||||
}
|
||||
|
||||
Ok(CallInfo {
|
||||
Some(CallInfo {
|
||||
place_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
@@ -497,8 +529,13 @@ pub enum CallState {
|
||||
}
|
||||
|
||||
/// Returns call state given the message ID.
|
||||
///
|
||||
/// Returns an error if the message is not a call message.
|
||||
pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
let call = context.load_call_by_id(msg_id).await?;
|
||||
let call = context
|
||||
.load_call_by_id(msg_id)
|
||||
.await?
|
||||
.with_context(|| format!("{msg_id} is not a call message"))?;
|
||||
let state = if call.is_incoming() {
|
||||
if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
@@ -53,7 +55,10 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
let info = t
|
||||
.load_call_by_id(m.id)
|
||||
.await?
|
||||
.expect("m should be a call message");
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
@@ -71,7 +76,10 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
|
||||
.await;
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
let info = t
|
||||
.load_call_by_id(m.id)
|
||||
.await?
|
||||
.expect("IncomingCall event should refer to a call message");
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
@@ -111,7 +119,10 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob.load_call_by_id(bob_call.id).await?;
|
||||
let info = bob
|
||||
.load_call_by_id(bob_call.id)
|
||||
.await?
|
||||
.expect("bob_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
|
||||
@@ -121,7 +132,10 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let info = bob2.load_call_by_id(bob2_call.id).await?;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
.await?
|
||||
.expect("bob2_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
|
||||
|
||||
@@ -140,7 +154,10 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
accept_call_info: ACCEPT_INFO.to_string()
|
||||
}
|
||||
);
|
||||
let info = alice.load_call_by_id(alice_call.id).await?;
|
||||
let info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
|
||||
@@ -460,14 +477,20 @@ async fn test_mark_calls() -> Result<()> {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
|
||||
let mut call_info: CallInfo = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(!call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
call_info.mark_as_accepted(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
|
||||
let mut call_info: CallInfo = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
@@ -484,7 +507,10 @@ async fn test_update_call_text() -> Result<()> {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let call_info = alice.load_call_by_id(alice_call.id).await?;
|
||||
let call_info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
call_info.update_text(&alice, "foo bar").await?;
|
||||
|
||||
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
@@ -529,3 +555,114 @@ async fn test_forward_call() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that "end call" message referring
|
||||
/// to a text message does not make receive_imf fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_end_text_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let received1 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
Hello\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received1.msg_ids.len(), 1);
|
||||
let msg = Message::load_from_db(alice, received1.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
|
||||
// Receiving "Call ended" message that refers
|
||||
// to the text message does not result in an error.
|
||||
let received2 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received2.msg_ids.len(), 1);
|
||||
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that partially downloaded "call ended"
|
||||
/// messages are not processed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_partial_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let seen = false;
|
||||
|
||||
// The messages in the test
|
||||
// have no `Date` on purpose,
|
||||
// so they are treated as new.
|
||||
let received_call = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call\n\
|
||||
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
|
||||
\n\
|
||||
Hello, this is a call\n",
|
||||
seen,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received_call.msg_ids.len(), 1);
|
||||
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(call_msg.viewtype, Viewtype::Call);
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
let imf_raw = b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n";
|
||||
receive_imf_from_inbox(
|
||||
alice,
|
||||
"second@example.net",
|
||||
imf_raw,
|
||||
seen,
|
||||
Some(imf_raw.len().try_into().unwrap()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// The call is still not ended.
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
// Fully downloading the message ends the call.
|
||||
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
|
||||
.await
|
||||
.context("Failed to fully download end call message")?;
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
26
src/chat.rs
26
src/chat.rs
@@ -373,7 +373,7 @@ impl ChatId {
|
||||
/// Returns true if the value was modified.
|
||||
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
|
||||
if self.is_special() {
|
||||
bail!("ignoring setting of Block-status for {}", self);
|
||||
bail!("ignoring setting of Block-status for {self}");
|
||||
}
|
||||
let count = context
|
||||
.sql
|
||||
@@ -702,8 +702,7 @@ impl ChatId {
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be special chat: {}",
|
||||
self
|
||||
"bad chat_id, can not be special chat: {self}"
|
||||
);
|
||||
|
||||
context
|
||||
@@ -813,8 +812,7 @@ impl ChatId {
|
||||
pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
self
|
||||
"bad chat_id, can not be a special chat: {self}"
|
||||
);
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
@@ -3145,8 +3143,7 @@ pub async fn send_text_msg(
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
chat_id
|
||||
"bad chat_id, can not be a special chat: {chat_id}"
|
||||
);
|
||||
|
||||
let mut msg = Message::new_text(text_to_send);
|
||||
@@ -3914,13 +3911,11 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||
"{} is not a group/broadcast where one can add members",
|
||||
chat_id
|
||||
"{chat_id} is not a group/broadcast where one can add members"
|
||||
);
|
||||
ensure!(
|
||||
Contact::real_exists_by_id(context, contact_id).await? || contact_id == ContactId::SELF,
|
||||
"invalid contact_id {} for adding to group",
|
||||
contact_id
|
||||
"invalid contact_id {contact_id} for adding to group"
|
||||
);
|
||||
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
||||
ensure!(
|
||||
@@ -4133,8 +4128,7 @@ pub async fn remove_contact_from_chat(
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"bad chat_id, can not be special chat: {}",
|
||||
chat_id
|
||||
"bad chat_id, can not be special chat: {chat_id}"
|
||||
);
|
||||
ensure!(
|
||||
!contact_id.is_special() || contact_id == ContactId::SELF,
|
||||
@@ -4148,7 +4142,7 @@ pub async fn remove_contact_from_chat(
|
||||
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
|
||||
bail!("{}", err_msg);
|
||||
bail!("{err_msg}");
|
||||
} else {
|
||||
let mut sync = Nosync;
|
||||
|
||||
@@ -4368,7 +4362,7 @@ pub async fn set_chat_profile_image(
|
||||
msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
|
||||
}
|
||||
chat.update_param(context).await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
if chat.is_promoted() {
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.emit_msgs_changed(chat_id, msg.id);
|
||||
}
|
||||
@@ -4390,7 +4384,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.await?;
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn test_chat_info() {
|
||||
"archived": false,
|
||||
"param": "",
|
||||
"is_sending_locations": false,
|
||||
"color": 29377,
|
||||
"color": 29381,
|
||||
"profile_image": {},
|
||||
"draft": "",
|
||||
"is_muted": false,
|
||||
@@ -1933,7 +1933,7 @@ async fn test_chat_get_color() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_ex(&t, None, "a chat").await?;
|
||||
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
|
||||
assert_eq!(color1, 0x613dd7);
|
||||
assert_eq!(color1, 0x6239dc);
|
||||
|
||||
// upper-/lowercase makes a difference for the colors, these are different groups
|
||||
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
||||
|
||||
@@ -28,7 +28,7 @@ fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
|
||||
pub fn str_to_color(s: &str) -> u32 {
|
||||
let lightness = 0.5;
|
||||
let chroma = 0.22;
|
||||
let chroma = 0.23;
|
||||
let angle = str_to_angle(s);
|
||||
let oklch = Oklch::new(lightness, chroma, angle);
|
||||
let rgb = oklch.to_rgb(TransferFunction::Srgb);
|
||||
|
||||
@@ -106,7 +106,7 @@ fn parse_server<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event.unescape().unwrap_or_default().trim().to_owned();
|
||||
let val = event.xml_content().unwrap_or_default().trim().to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
|
||||
@@ -79,7 +79,7 @@ fn parse_protocol<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
@@ -123,7 +123,7 @@ fn parse_redirecturl<B: BufRead>(
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
|
||||
@@ -1582,6 +1582,8 @@ impl Contact {
|
||||
pub fn get_color(&self) -> u32 {
|
||||
if let Some(fingerprint) = self.fingerprint() {
|
||||
str_to_color(&fingerprint.hex())
|
||||
} else if self.id == ContactId::SELF {
|
||||
0x808080
|
||||
} else {
|
||||
str_to_color(&self.addr.to_lowercase())
|
||||
}
|
||||
@@ -1742,8 +1744,7 @@ pub(crate) async fn set_blocked(
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!contact_id.is_special(),
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
"Can't block special contact {contact_id}"
|
||||
);
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::*;
|
||||
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
|
||||
#[test]
|
||||
@@ -759,7 +760,7 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
|
||||
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
|
||||
assert_eq!(color1, 0x4947dc);
|
||||
assert_eq!(color1, 0x4844e2);
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
||||
@@ -773,6 +774,20 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_color_vs_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
t.configure_addr("alice@example.org").await;
|
||||
assert!(t.is_configured().await?);
|
||||
let color = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_eq!(color, 0x808080);
|
||||
get_securejoin_qr(t, None).await?;
|
||||
let color1 = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_ne!(color1, color);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::LazyLock;
|
||||
|
||||
use quick_xml::{
|
||||
Reader,
|
||||
errors::Error as QuickXmlError,
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
};
|
||||
|
||||
@@ -132,6 +133,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
reader.config_mut().check_end_names = false;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut char_buf = String::with_capacity(4);
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
@@ -140,16 +142,9 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(e)) => match e.escape() {
|
||||
Ok(e) => dehtml_text_cb(&e, &mut dehtml),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"CDATA escape error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(quick_xml::events::Event::CData(e)) => {
|
||||
str_cb(&String::from_utf8_lossy(&e as &[_]), &mut dehtml)
|
||||
}
|
||||
Ok(quick_xml::events::Event::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
@@ -159,6 +154,33 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
&mut dehtml,
|
||||
);
|
||||
}
|
||||
Ok(quick_xml::events::Event::GeneralRef(ref e)) => {
|
||||
match e.resolve_char_ref() {
|
||||
Err(err) => eprintln!(
|
||||
"resolve_char_ref() error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
err,
|
||||
),
|
||||
Ok(Some(ch)) => {
|
||||
char_buf.clear();
|
||||
char_buf.push(ch);
|
||||
str_cb(&char_buf, &mut dehtml);
|
||||
}
|
||||
Ok(None) => {
|
||||
let event_str = String::from_utf8_lossy(e);
|
||||
if let Some(s) = quick_xml::escape::resolve_html5_entity(&event_str) {
|
||||
str_cb(s, &mut dehtml);
|
||||
} else {
|
||||
// Nonstandard entity. Add escaped.
|
||||
str_cb(&format!("&{event_str};"), &mut dehtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(QuickXmlError::IllFormed(_)) => {
|
||||
// This is probably not HTML at all and should be left as is.
|
||||
str_cb(&String::from_utf8_lossy(&buf), &mut dehtml);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Parse html error: Error at position {}: {:?}",
|
||||
@@ -176,36 +198,36 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let event = event as &[_];
|
||||
let event_str = std::str::from_utf8(event).unwrap_or_default();
|
||||
let mut last_added = escaper::decode_html_buf_sloppy(event).unwrap_or_default();
|
||||
if event_str.starts_with(&last_added) {
|
||||
last_added = event_str.to_string();
|
||||
str_cb(event_str, dehtml);
|
||||
}
|
||||
}
|
||||
|
||||
fn str_cb(event_str: &str, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
let add_text = dehtml.get_add_text();
|
||||
if add_text == AddText::YesRemoveLineEnds {
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let event_str = LINE_RE.replace_all(event_str, " ");
|
||||
|
||||
// Add a space if `event_str` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `event_str`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && event_str.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let last_added = LINE_RE.replace_all(&last_added, " ");
|
||||
|
||||
// Add a space if `last_added` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `last_added`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
*buf += last_added.trim_start();
|
||||
} else {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
|
||||
}
|
||||
*buf += event_str.trim_start();
|
||||
} else if add_text == AddText::YesPreserveLineEnds {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(event_str, "\n").as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,8 +283,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -543,4 +544,43 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that fully downloading the message
|
||||
/// works even if the Message-ID already exists
|
||||
/// in the database assigned to the trash chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_trashed() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let imf_raw = b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
// Download message from Bob partially.
|
||||
let partial_received_msg =
|
||||
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, Some(100000))
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(partial_received_msg.msg_ids.len(), 1);
|
||||
|
||||
// Delete the received message.
|
||||
// Not it is still in the database,
|
||||
// but in the trash chat.
|
||||
delete_msgs(alice, &[partial_received_msg.msg_ids[0]]).await?;
|
||||
|
||||
// Fully download message after deletion.
|
||||
let full_received_msg =
|
||||
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, None).await?;
|
||||
|
||||
// The message does not reappear.
|
||||
// However, `receive_imf` should not fail.
|
||||
assert!(full_received_msg.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,10 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// For each message a row ID, chat id, viewtype and location ID is returned.
|
||||
///
|
||||
/// Unknown viewtypes are returned as `Viewtype::Unknown`
|
||||
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
|
||||
/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source)
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
@@ -395,7 +399,11 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for ephemeral handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
@@ -437,7 +445,11 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for delete-old handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|
||||
13
src/key.rs
13
src/key.rs
@@ -15,6 +15,7 @@ use rand::thread_rng;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
@@ -414,15 +415,11 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
|
||||
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
|
||||
(new_key_id,),
|
||||
)?;
|
||||
Ok(Some(new_key_id))
|
||||
Ok(new_key_id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(new_key_id) = new_key_id {
|
||||
// Update config cache if transaction succeeded and changed current default key.
|
||||
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
|
||||
}
|
||||
|
||||
context.emit_event(EventType::AccountsItemChanged);
|
||||
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -500,7 +497,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, "wrong fingerprint length: {}", hex_repr);
|
||||
ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}");
|
||||
let fp = Fingerprint::new(v);
|
||||
Ok(fp)
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Kml {
|
||||
if self.tag == KmlTag::PlacemarkTimestampWhen
|
||||
|| self.tag == KmlTag::PlacemarkPointCoordinates
|
||||
{
|
||||
let val = event.unescape().unwrap_or_default();
|
||||
let val = event.xml_content().unwrap_or_default();
|
||||
|
||||
let val = val.replace(['\n', '\r', '\t', ' '], "");
|
||||
|
||||
|
||||
@@ -490,8 +490,7 @@ impl Message {
|
||||
pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
|
||||
ensure!(
|
||||
!id.is_special(),
|
||||
"Can not load special message ID {} from DB",
|
||||
id
|
||||
"Can not load special message ID {id} from DB"
|
||||
);
|
||||
let msg = context
|
||||
.sql
|
||||
|
||||
@@ -419,10 +419,7 @@ impl MimeFactory {
|
||||
None
|
||||
} else {
|
||||
if keys.is_empty() && !recipients.is_empty() {
|
||||
bail!(
|
||||
"No recipient keys are available, cannot encrypt to {:?}.",
|
||||
recipients
|
||||
);
|
||||
bail!("No recipient keys are available, cannot encrypt to {recipients:?}.");
|
||||
}
|
||||
|
||||
// Remove recipients for which the key is missing.
|
||||
|
||||
@@ -2080,7 +2080,7 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
if let Some(id) = parse_message_ids(ids).first() {
|
||||
Ok(id.to_string())
|
||||
} else {
|
||||
bail!("could not parse message_id: {}", ids);
|
||||
bail!("could not parse message_id: {ids}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ impl str::FromStr for Params {
|
||||
inner.insert(key, value);
|
||||
}
|
||||
} else {
|
||||
bail!("Not a key-value pair: {:?}", line);
|
||||
bail!("Not a key-value pair: {line:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src/qr.rs
11
src/qr.rs
@@ -408,7 +408,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
|
||||
match percent_decode_str(&encoded_name).decode_utf8() {
|
||||
Ok(name) => name.to_string(),
|
||||
Err(err) => bail!("Invalid name: {}", err),
|
||||
Err(err) => bail!("Invalid name: {err}"),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
@@ -432,7 +432,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
|
||||
match percent_decode_str(&encoded_name).decode_utf8() {
|
||||
Ok(name) => Some(name.to_string()),
|
||||
Err(err) => bail!("Invalid group name: {}", err),
|
||||
Err(err) => bail!("Invalid group name: {err}"),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -581,7 +581,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
}
|
||||
|
||||
let Some(host) = host else {
|
||||
bail!("Bad t.me/socks url: {:?}", url);
|
||||
bail!("Bad t.me/socks url: {url:?}");
|
||||
};
|
||||
|
||||
let mut url = "socks5://".to_string();
|
||||
@@ -684,10 +684,7 @@ pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(
|
||||
context.emit_event(EventType::Error(format!(
|
||||
"Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
|
||||
)));
|
||||
bail!(
|
||||
"Cannot create account, unexpected server response:\n{:?}",
|
||||
response_text
|
||||
)
|
||||
bail!("Cannot create account, unexpected server response:\n{response_text:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
bail!("Bad scheme for account URL: {:?}.", payload);
|
||||
bail!("Bad scheme for account URL: {payload:?}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
|
||||
Some("starttls") => Some(Socket::Starttls),
|
||||
Some("default") => Some(Socket::Automatic),
|
||||
Some("plain") => Some(Socket::Plain),
|
||||
Some(other) => bail!("Unknown security level: {}", other),
|
||||
Some(other) => bail!("Unknown security level: {other}"),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
@@ -152,7 +152,7 @@ fn parse_certificate_checks(
|
||||
Some("1") => Some(EnteredCertificateChecks::Strict),
|
||||
Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
|
||||
Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
|
||||
Some(other) => bail!("Unknown certificatecheck level: {}", other),
|
||||
Some(other) => bail!("Unknown certificatecheck level: {other}"),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -557,13 +557,17 @@ pub(crate) async fn receive_imf_inner(
|
||||
// make sure, this check is done eg. before securejoin-processing.
|
||||
let (replace_msg_id, replace_chat_id);
|
||||
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
let msg = Message::load_from_db(context, old_msg_id).await?;
|
||||
replace_msg_id = Some(old_msg_id);
|
||||
replace_chat_id = if msg.download_state() != DownloadState::Done {
|
||||
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
|
||||
.await?
|
||||
.filter(|msg| msg.download_state() != DownloadState::Done)
|
||||
{
|
||||
// the message was partially downloaded before and is fully downloaded now.
|
||||
info!(context, "Message already partly in DB, replacing.");
|
||||
Some(msg.chat_id)
|
||||
} else {
|
||||
// The message was already fully downloaded
|
||||
// or cannot be loaded because it is deleted.
|
||||
None
|
||||
};
|
||||
} else {
|
||||
@@ -995,7 +999,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.is_call() {
|
||||
if is_partial_download.is_none() && mime_parser.is_call() {
|
||||
context
|
||||
.handle_call_msg(insert_msg_id, &mime_parser, from_id)
|
||||
.await?;
|
||||
@@ -1153,8 +1157,9 @@ async fn decide_chat_assignment(
|
||||
{
|
||||
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
|
||||
true
|
||||
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded
|
||||
} else if is_partial_download.is_none()
|
||||
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded)
|
||||
{
|
||||
info!(context, "Call state changed (TRASH).");
|
||||
true
|
||||
@@ -1982,8 +1987,9 @@ async fn add_parts(
|
||||
|
||||
handle_edit_delete(context, mime_parser, from_id).await?;
|
||||
|
||||
if mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded
|
||||
if is_partial_download.is_none()
|
||||
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded)
|
||||
{
|
||||
if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
if let Some(call) =
|
||||
|
||||
@@ -932,7 +932,7 @@ impl Scheduler {
|
||||
|
||||
// wait for all loops to be started
|
||||
if let Err(err) = try_join_all(start_recvs).await {
|
||||
bail!("failed to start scheduler: {}", err);
|
||||
bail!("failed to start scheduler: {err}");
|
||||
}
|
||||
|
||||
info!(ctx, "scheduler is running");
|
||||
|
||||
@@ -242,7 +242,7 @@ pub(crate) async fn smtp_send(
|
||||
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
|
||||
// should definitely go here, because user has to open the link to
|
||||
// resume message sending.
|
||||
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
|
||||
SendResult::Failure(format_err!("Permanent SMTP error: {err}"))
|
||||
}
|
||||
}
|
||||
async_smtp::error::Error::Transient(ref response) => {
|
||||
@@ -471,7 +471,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SendResult::Failure(err) => Err(format_err!("{}", err)),
|
||||
SendResult::Failure(err) => Err(format_err!("{err}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +586,7 @@ async fn send_mdn_rfc724_mid(
|
||||
|
||||
let addr = contact.get_addr();
|
||||
let recipient = async_smtp::EmailAddress::new(addr.to_string())
|
||||
.map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err))?;
|
||||
.map_err(|err| format_err!("invalid recipient: {addr} {err:?}"))?;
|
||||
let recipients = vec![recipient];
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
|
||||
@@ -69,7 +69,7 @@ pub(crate) async fn connect_and_auth(
|
||||
.await
|
||||
.context("SMTP failed to get OAUTH2 access token")?;
|
||||
if access_token.is_none() {
|
||||
bail!("SMTP OAuth 2 error {}", addr);
|
||||
bail!("SMTP OAuth 2 error {addr}");
|
||||
}
|
||||
(
|
||||
async_smtp::authentication::Credentials::new(
|
||||
|
||||
@@ -279,7 +279,7 @@ impl Context {
|
||||
};
|
||||
|
||||
if !valid {
|
||||
bail!("{} is not a valid webxdc file", filename);
|
||||
bail!("{filename} is not a valid webxdc file");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -837,8 +837,8 @@ fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
|
||||
}
|
||||
|
||||
async fn get_blob(archive: &mut SeekZipFileReader<BufReader<File>>, name: &str) -> Result<Vec<u8>> {
|
||||
let (i, _) = find_zip_entry(archive.file(), name)
|
||||
.ok_or_else(|| anyhow!("no entry found for {}", name))?;
|
||||
let (i, _) =
|
||||
find_zip_entry(archive.file(), name).ok_or_else(|| anyhow!("no entry found for {name}"))?;
|
||||
let mut reader = archive.reader_with_entry(i).await?;
|
||||
let mut buf = Vec::new();
|
||||
reader.read_to_end_checked(&mut buf).await?;
|
||||
|
||||
Reference in New Issue
Block a user