mirror of
https://github.com/chatmail/core.git
synced 2026-07-01 20:14:57 +03:00
Compare commits
1 Commits
cli-displa
...
link2xt/hi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d31be3165 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -139,12 +139,12 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo nextest run --workspace --locked
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --locked --doc
|
||||
run: cargo test --workspace --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -228,9 +228,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -281,11 +281,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
- os: windows-latest
|
||||
python: 3.14
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
|
||||
8
.github/workflows/deltachat-rpc-server.yml
vendored
8
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
|
||||
13
.github/workflows/nix.yml
vendored
13
.github/workflows/nix.yml
vendored
@@ -5,12 +5,10 @@ on:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -25,8 +23,11 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix fmt
|
||||
|
||||
# Check that formatting does not change anything.
|
||||
- run: git diff --exit-code
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
@@ -84,7 +85,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -104,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
116
CHANGELOG.md
116
CHANGELOG.md
@@ -1,107 +1,5 @@
|
||||
# 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
|
||||
|
||||
- [**breaking**] Remove APIs for video chat invitations.
|
||||
|
||||
### CI
|
||||
|
||||
- nix: Run the workflow when workflow file changes.
|
||||
- nix: Switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- No implicit member changes from old Delta Chat clients ([#7220](https://github.com/chatmail/core/pull/7220)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to load messages with unknown viewtype.
|
||||
- Only omit group changes messages if SELF is really added ([#7220](https://github.com/chatmail/core/pull/7220)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Assert that Iroh node addresses have home relay URL.
|
||||
|
||||
## [2.17.0] - 2025-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove deprecated verified_one_on_one_chats config.
|
||||
|
||||
### CI
|
||||
|
||||
- Require that Cargo.lock is up to date.
|
||||
- Fix CI checking Nix formatting.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Comment about outdated timespan.
|
||||
- Clarify CALL events ([#7188](https://github.com/chatmail/core/pull/7188)).
|
||||
- Add docs for JS `BaseDeltaChat`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make `text/calendar` alternative available as an attachment.
|
||||
- Better summary for calls.
|
||||
- Add strings 'You left the channel.' and 'Scan to join Channel' ([#7266](https://github.com/chatmail/core/pull/7266)).
|
||||
- Stock strings for calls.
|
||||
- ffi: Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prefer last part in `multipart/alternative`.
|
||||
- Prefetch messages in limited batches ([#6915](https://github.com/chatmail/core/pull/6915)).
|
||||
- Forward calls as text messages.
|
||||
- Consistent spelling of "canceled" with a single "l".
|
||||
- Lowercase "call" in "Missed call" and similar strings.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Return the reason when failing to place calls.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test reception of `multipart/alternative` with `text/calendar`.
|
||||
|
||||
## [2.16.0] - 2025-10-01
|
||||
|
||||
### API-Changes
|
||||
@@ -1866,7 +1764,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
### Fixes
|
||||
|
||||
- Reset quota on configured address change ([#5908](https://github.com/chatmail/core/pull/5908)).
|
||||
- Do not emit progress 1000 when configuration is canceled.
|
||||
- Do not emit progress 1000 when configuration is cancelled.
|
||||
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/chatmail/core/pull/5338)).
|
||||
- Re-add tokens.foreign_id column ([#6038](https://github.com/chatmail/core/pull/6038)).
|
||||
|
||||
@@ -4314,7 +4212,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/chatmail/core/pull/4390)).
|
||||
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
|
||||
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/chatmail/core/pull/4377)).
|
||||
- Delete `smtp` rows when message sending is canceled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -4325,7 +4223,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
### Fixes
|
||||
|
||||
- Fetch at most 100 existing messages even if EXISTS was not received.
|
||||
- Delete `smtp` rows when message sending is canceled.
|
||||
- Delete `smtp` rows when message sending is cancelled.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -4412,14 +4310,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
## [1.112.3] - 2023-03-30
|
||||
|
||||
### Fixes
|
||||
- `transfer::get_backup` now frees ongoing process when canceled. #4249
|
||||
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
|
||||
|
||||
## [1.112.2] - 2023-03-30
|
||||
|
||||
### Changes
|
||||
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
|
||||
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
|
||||
- Make sure BackupProvider is canceled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
|
||||
### Fixes
|
||||
- Do not return media from trashed messages in the "All media" view. #4247
|
||||
@@ -6917,7 +6815,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.14.0]: https://github.com/chatmail/core/compare/v2.13.0..v2.14.0
|
||||
[2.15.0]: https://github.com/chatmail/core/compare/v2.14.0..v2.15.0
|
||||
[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
|
||||
|
||||
@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
|
||||
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -1289,7 +1289,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1399,7 +1399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1421,7 +1421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1437,7 +1437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1466,7 +1466,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -4639,9 +4639,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.3"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5289,11 +5289,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdp"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69"
|
||||
checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"substring",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.20.0"
|
||||
version = "2.16.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 = { version = "0.38", features = ["escape-html"] }
|
||||
quick-xml = "0.37"
|
||||
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 {input:?} must not contain whitespaces, '>' or '<'");
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
}
|
||||
|
||||
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 {input:?} must contain '@' character"),
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -458,6 +458,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* The library uses the `media_quality` setting to use different defaults
|
||||
* for recoding images sent with type #DC_MSG_IMAGE.
|
||||
* If needed, recoding other file types is up to the UI.
|
||||
* - `webrtc_instance` = webrtc instance to use for videochats in the form
|
||||
* `[basicwebrtc:|jitsi:]https://example.com/subdir#roomname=$ROOM`
|
||||
* if the URL is prefixed by `basicwebrtc`, the server is assumed to be of the type
|
||||
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
|
||||
* The type `jitsi:` may be handled by external apps.
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
* - `bot` = Set to "1" if this is a bot.
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages,
|
||||
@@ -569,10 +575,11 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
|
||||
/**
|
||||
* Set configuration values from a QR code.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_LOGIN.
|
||||
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN.
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -1045,6 +1052,42 @@ void dc_send_edit_request (dc_context_t* context, uint32_t ms
|
||||
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
* This function reads the `webrtc_instance` config value,
|
||||
* may check that the server is working in some way
|
||||
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
|
||||
*
|
||||
* After that, the function sends out a message that contains information to join the room:
|
||||
*
|
||||
* - To allow non-delta-clients to join the chat,
|
||||
* the message contains a text-area with some descriptive text
|
||||
* and a URL that can be opened in a supported browser to join the videochat.
|
||||
*
|
||||
* - delta-clients can get all information needed from
|
||||
* the message object, using e.g.
|
||||
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* dc_send_videochat_invitation() is blocking and may take a while,
|
||||
* so the UIs will typically call the function from within a thread.
|
||||
* Moreover, UIs will typically enter the room directly without an additional click on the message,
|
||||
* for this purpose, the function returns the message id directly.
|
||||
*
|
||||
* As for other messages sent, this function
|
||||
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
|
||||
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
|
||||
* However, UIs might some things differently, e.g. play a different sound.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to start a videochat for.
|
||||
* @return The ID of the message sent out
|
||||
* or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -1179,7 +1222,7 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* Possible actions during ringing:
|
||||
*
|
||||
* - caller cancels the call using dc_end_call():
|
||||
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed call"
|
||||
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed Call"
|
||||
*
|
||||
* - callee accepts using dc_accept_incoming_call():
|
||||
* caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED.
|
||||
@@ -1187,20 +1230,19 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
*
|
||||
* - callee declines using dc_end_call():
|
||||
* caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call".
|
||||
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Canceled Call",
|
||||
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Cancelled Call",
|
||||
*
|
||||
* - callee is already in a call:
|
||||
* what to do depends on the capabilities of UI to handle calls.
|
||||
* if UI cannot handle multiple calls, an easy approach would be to decline the new call automatically
|
||||
* and make that visble to the user in the call, e.g. by a notification
|
||||
* in this case, UI may decide to show a notification instead of ringing.
|
||||
* otherwise, this is same as timeout
|
||||
*
|
||||
* - timeout:
|
||||
* after 1 minute without action,
|
||||
* caller and callee receive #DC_EVENT_CALL_ENDED
|
||||
* to prevent endless ringing of callee
|
||||
* in case caller got offline without being able to send cancellation message.
|
||||
* for caller, this is a "Canceled call";
|
||||
* for callee, this is a "Missed call"
|
||||
* for caller, this is a "Cancelled Call";
|
||||
* for callee, this is a "Missed Call"
|
||||
*
|
||||
* Actions during the call:
|
||||
*
|
||||
@@ -1210,13 +1252,6 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* - callee ends the call using dc_end_call():
|
||||
* caller receives #DC_EVENT_CALL_ENDED
|
||||
*
|
||||
* Contact request handling:
|
||||
*
|
||||
* - placing or accepting calls implies accepting contact requests
|
||||
*
|
||||
* - ending a call does not accept a contact request;
|
||||
* instead, the call will timeout on all affected devices.
|
||||
*
|
||||
* Note, that the events are for updating the call screen,
|
||||
* possible status messages are added and updated as usual, including the known events.
|
||||
* In the UI, the sorted chatlist is used as an overview about calls as well as messages.
|
||||
@@ -1244,7 +1279,6 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch
|
||||
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
|
||||
*
|
||||
* If the call is already accepted or ended, nothing happens.
|
||||
* If the chat is a contact request, it is accepted implicitly.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -1265,12 +1299,7 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch
|
||||
* Unaccepted calls ended by the callee are a "decline".
|
||||
* If the call was accepted, this is a "hangup".
|
||||
*
|
||||
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED unless they are contact requests.
|
||||
* For contact requests, the call times out on all other affected devices.
|
||||
*
|
||||
* If the message ID is wrong or does not exist for whatever reasons, nothing happens.
|
||||
* Therefore, and for resilience, UI should remove the call UI directly when calling
|
||||
* this function and not only on the event.
|
||||
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED.
|
||||
*
|
||||
* If the call is already ended, nothing happens.
|
||||
*
|
||||
@@ -2572,6 +2601,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_BACKUP 251 // deprecated
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2625,6 +2655,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* show a hint to the user that this backup comes from a newer Delta Chat version
|
||||
* and this device needs an update
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_PROXY with dc_lot_t::text1=address:
|
||||
* ask the user if they want to use the given proxy.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
@@ -4696,6 +4730,22 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get URL of a videochat invitation.
|
||||
*
|
||||
* Videochat invitations are sent out using dc_send_videochat_invitation()
|
||||
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return If the message contains a videochat invitation,
|
||||
* the URL of the invitation is returned.
|
||||
* If the message is no videochat invitation, NULL is returned.
|
||||
* Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the error status of the message.
|
||||
* If there is no error associated with the message, NULL is returned.
|
||||
@@ -4718,6 +4768,41 @@ char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
char* dc_msg_get_error (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get type of videochat.
|
||||
*
|
||||
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
|
||||
* in this case, if `basicwebrtc:` as of https://github.com/cracker0dks/basicwebrtc or `jitsi`
|
||||
* were used to initiate the videochat,
|
||||
* dc_msg_get_videochat_type() returns the corresponding type.
|
||||
*
|
||||
* The videochat URL can be retrieved using dc_msg_get_videochat_url().
|
||||
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC, DC_VIDEOCHATTYPE_JITSI or DC_VIDEOCHATTYPE_UNKNOWN.
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
|
||||
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
|
||||
* // videochat invitation that we ship a client for
|
||||
* } else {
|
||||
* // use browser for videochat - or add an additional check for DC_VIDEOCHATTYPE_JITSI
|
||||
* }
|
||||
* } else {
|
||||
* // not a videochat invitation
|
||||
* }
|
||||
* ~~~
|
||||
*/
|
||||
int dc_msg_get_videochat_type (const dc_msg_t* msg);
|
||||
|
||||
#define DC_VIDEOCHATTYPE_UNKNOWN 0
|
||||
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
|
||||
#define DC_VIDEOCHATTYPE_JITSI 2
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the message has a full HTML version.
|
||||
*
|
||||
@@ -5616,20 +5701,19 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_MSG_FILE 60
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing videochat.
|
||||
* The message was created via dc_send_videochat_invitation() on this or a remote device.
|
||||
*
|
||||
* Typically, such messages are rendered differently by the UIs,
|
||||
* e.g. contain a button to join the videochat.
|
||||
* The URL for joining can be retrieved using dc_msg_get_videochat_url().
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing call.
|
||||
*
|
||||
* These messages are created by dc_place_outgoing_call()
|
||||
* and should be rendered by UI similar to text messages,
|
||||
* maybe with some "phone icon" at the side.
|
||||
*
|
||||
* The message text is updated as needed
|
||||
* and UI will be informed via #DC_EVENT_MSGS_CHANGED as usual.
|
||||
*
|
||||
* Do not start ringing when seeing this message;
|
||||
* the mesage may belong e.g. to an old missed call.
|
||||
*
|
||||
* Instead, ringing should start on the event #DC_EVENT_INCOMING_CALL
|
||||
*/
|
||||
#define DC_MSG_CALL 71
|
||||
|
||||
@@ -6637,8 +6721,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
*
|
||||
* Together with this event,
|
||||
* a message of type #DC_MSG_CALL is added to the corresponding chat;
|
||||
* this message is announced and updated by the usual event as #DC_EVENT_MSGS_CHANGED,
|
||||
* there is usually no need to take care of this message from any of the CALL events.
|
||||
* this message is announced and updated by the usual even as #DC_EVENT_MSGS_CHANGED.
|
||||
*
|
||||
* If user takes action, dc_accept_incoming_call() or dc_end_call() should be called.
|
||||
*
|
||||
@@ -6655,7 +6738,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* The callee accepted an incoming call on this or another device using dc_accept_incoming_call().
|
||||
* The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
* The event is sent unconditionally when the corresponding message is received.
|
||||
* UI should only take action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
*/
|
||||
@@ -6664,7 +6748,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
/**
|
||||
* A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call().
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
* The event is sent unconditionally when the corresponding message is received.
|
||||
* UI should only take action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
* @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call()
|
||||
@@ -6672,10 +6757,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
|
||||
|
||||
/**
|
||||
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
|
||||
* An incoming or outgoing call was ended using dc_end_call().
|
||||
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
* The event is sent unconditionally when the corresponding message is received.
|
||||
* UI should only take action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
*/
|
||||
@@ -7166,6 +7252,17 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
|
||||
/// "Video chat invitation"
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_VIDEOCHAT_INVITATION 82
|
||||
|
||||
/// "You are invited to a video chat, click %1$s to join."
|
||||
///
|
||||
/// Used as message text of outgoing video chat invitations.
|
||||
/// - %1$s will be replaced by the URL of the video chat
|
||||
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
|
||||
|
||||
/// "Error: %1$s"
|
||||
///
|
||||
/// Used in error strings.
|
||||
@@ -7464,7 +7561,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
||||
|
||||
/// "You left the group."
|
||||
/// "You left."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_LEFT_BY_YOU 132
|
||||
@@ -7687,12 +7784,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the provider's domain.
|
||||
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
|
||||
|
||||
/// "⚠️ 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."
|
||||
///
|
||||
/// Added to the device chat if could not decrypt a new outgoing message (i.e. not when fetching
|
||||
/// existing messages). But no more than once a day.
|
||||
#define DC_STR_CANT_DECRYPT_OUTGOING_MSGS 175
|
||||
|
||||
/// "You reacted %1$s to '%2$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||
@@ -7729,32 +7820,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||
#define DC_STR_DONATION_REQUEST 193
|
||||
|
||||
/// "Outgoing call"
|
||||
#define DC_STR_OUTGOING_CALL 194
|
||||
|
||||
/// "Incoming call"
|
||||
#define DC_STR_INCOMING_CALL 195
|
||||
|
||||
/// "Declined call"
|
||||
#define DC_STR_DECLINED_CALL 196
|
||||
|
||||
/// "Canceled call"
|
||||
#define DC_STR_CANCELED_CALL 197
|
||||
|
||||
/// "Missed call"
|
||||
#define DC_STR_MISSED_CALL 198
|
||||
|
||||
/// "You left the channel."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_CHANNEL_LEFT_BY_YOU 200
|
||||
|
||||
/// "Scan to join channel %1$s"
|
||||
///
|
||||
/// Subtitle for channel join qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced with the channel name.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
/**
|
||||
* @}
|
||||
|
||||
@@ -1098,6 +1098,25 @@ pub unsafe extern "C" fn dc_send_delete_request(
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3835,6 +3854,31 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
ffi_msg.message.has_html().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.get_videochat_url()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -51,6 +51,7 @@ impl Lot {
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||
@@ -104,6 +105,7 @@ impl Lot {
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -130,6 +132,7 @@ impl Lot {
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -182,6 +185,9 @@ pub enum LotState {
|
||||
|
||||
QrBackupTooNew = 255,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// text1=address, text2=protocol
|
||||
QrProxy = 271,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.20.0"
|
||||
version = "2.16.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 {id} not found"))?;
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
@@ -308,7 +308,8 @@ impl CommandApi {
|
||||
Ok(Account::from_context(&ctx, account_id).await?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"account with id {account_id} doesn't exist anymore"
|
||||
"account with id {} doesn't exist anymore",
|
||||
account_id
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1808,13 +1809,13 @@ impl CommandApi {
|
||||
|
||||
/// Offers a backup for remote devices to retrieve.
|
||||
///
|
||||
/// Can be canceled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
|
||||
/// failure.
|
||||
///
|
||||
/// This **stops IO** while it is running.
|
||||
///
|
||||
/// Returns once a remote device has retrieved the backup, or is canceled.
|
||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1880,7 +1881,7 @@ impl CommandApi {
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be canceled by stopping the ongoing process.
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
///
|
||||
/// Do not forget to call start_io on the account after a successful import,
|
||||
/// otherwise it will not connect to the email server.
|
||||
@@ -2281,6 +2282,13 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// misc prototyping functions
|
||||
// that might get removed later again
|
||||
@@ -2311,7 +2319,8 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
ensure!(
|
||||
message.get_viewtype() == Viewtype::Sticker,
|
||||
"message {msg_id} is not a sticker"
|
||||
"message {} is not a sticker",
|
||||
msg_id
|
||||
);
|
||||
let account_folder = ctx
|
||||
.get_dbfile()
|
||||
@@ -2531,7 +2540,10 @@ impl CommandApi {
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
||||
use deltachat::context::Context;
|
||||
@@ -20,15 +20,13 @@ pub struct JsonrpcCallInfo {
|
||||
|
||||
/// Call state.
|
||||
///
|
||||
/// For example, if the call is accepted, active, canceled, declined etc.
|
||||
/// For example, if the call is accepted, active, cancelled, declined etc.
|
||||
pub state: JsonrpcCallState,
|
||||
}
|
||||
|
||||
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?.with_context(|| {
|
||||
format!("Attempting to get call state of non-call message {msg_id}")
|
||||
})?;
|
||||
let call_info = context.load_call_by_id(msg_id).await?;
|
||||
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?;
|
||||
@@ -70,13 +68,13 @@ pub enum JsonrpcCallState {
|
||||
/// that was declined before the timeout.
|
||||
Declined,
|
||||
|
||||
/// Outgoing call that has been canceled on our side
|
||||
/// Outgoing call that has been cancelled on our side
|
||||
/// before receiving a response.
|
||||
///
|
||||
/// Incoming calls cannot be canceled,
|
||||
/// on the receiver side canceled calls
|
||||
/// Incoming calls cannot be cancelled,
|
||||
/// on the receiver side cancelled calls
|
||||
/// usually result in missed calls.
|
||||
Canceled,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl JsonrpcCallState {
|
||||
@@ -89,7 +87,7 @@ impl JsonrpcCallState {
|
||||
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
|
||||
CallState::Missed => JsonrpcCallState::Missed,
|
||||
CallState::Declined => JsonrpcCallState::Declined,
|
||||
CallState::Canceled => JsonrpcCallState::Canceled,
|
||||
CallState::Cancelled => JsonrpcCallState::Cancelled,
|
||||
};
|
||||
|
||||
Ok(jsonrpc_call_state)
|
||||
|
||||
@@ -84,6 +84,9 @@ pub struct MessageObject {
|
||||
dimensions_height: i32,
|
||||
dimensions_width: i32,
|
||||
|
||||
videochat_type: Option<u32>,
|
||||
videochat_url: Option<String>,
|
||||
|
||||
override_sender_name: Option<String>,
|
||||
sender: ContactObject,
|
||||
|
||||
@@ -236,6 +239,15 @@ impl MessageObject {
|
||||
dimensions_height: message.get_height(),
|
||||
dimensions_width: message.get_width(),
|
||||
|
||||
videochat_type: match message.get_videochat_type() {
|
||||
Some(vct) => Some(
|
||||
vct.to_u32()
|
||||
.context("videochat type conversion to number failed")?,
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
videochat_url: message.get_videochat_url(),
|
||||
|
||||
override_sender_name,
|
||||
sender,
|
||||
|
||||
@@ -309,6 +321,9 @@ pub enum MessageViewtype {
|
||||
/// Message containing any file, eg. a PDF.
|
||||
File,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation,
|
||||
|
||||
/// Message is a call.
|
||||
Call,
|
||||
|
||||
@@ -333,6 +348,7 @@ impl From<Viewtype> for MessageViewtype {
|
||||
Viewtype::Voice => MessageViewtype::Voice,
|
||||
Viewtype::Video => MessageViewtype::Video,
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Call => MessageViewtype::Call,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||
@@ -352,6 +368,7 @@ impl From<MessageViewtype> for Viewtype {
|
||||
MessageViewtype::Voice => Viewtype::Voice,
|
||||
MessageViewtype::Video => Viewtype::Video,
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Call => Viewtype::Call,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||
|
||||
@@ -225,6 +225,13 @@ impl From<Qr> for QrObject {
|
||||
auth_token,
|
||||
},
|
||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
} => QrObject::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.20.0"
|
||||
"version": "2.16.0"
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@ export class BaseDeltaChat<
|
||||
|
||||
constructor(
|
||||
public transport: Transport,
|
||||
/**
|
||||
* Whether to start calling {@linkcode RawClient.getNextEvent}
|
||||
* and emitting the respective events on this class.
|
||||
*/
|
||||
startEventLoop: boolean,
|
||||
) {
|
||||
super();
|
||||
@@ -48,9 +44,6 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see the constructor's `startEventLoop`
|
||||
*/
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
@@ -69,17 +62,10 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
|
||||
*/
|
||||
async listAccounts(): Promise<T.Account[]> {
|
||||
return await this.rpc.getAllAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience function to listen on events binned by `account_id`
|
||||
* (see {@linkcode RawClient.getAllAccounts}).
|
||||
*/
|
||||
getContextEvents(account_id: number) {
|
||||
if (this.contextEmitters[account_id]) {
|
||||
return this.contextEmitters[account_id];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -210,7 +210,13 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
msg.get_videochat_url().unwrap_or_default(),
|
||||
msg.get_videochat_type().unwrap_or_default()
|
||||
)
|
||||
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
match msg.get_webxdc_info(context).await {
|
||||
Ok(info) => format!(
|
||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||
@@ -365,6 +371,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
sendupdate <msg-id> <json status update>\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
listmedia\n\
|
||||
@@ -418,7 +425,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 +439,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
bail!("{} is no setup message.", msg_id,);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
@@ -527,7 +534,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -955,6 +962,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
}
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
@@ -1287,7 +1298,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 38] = [
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -206,6 +206,7 @@ const CHAT_COMMANDS: [&str; 38] = [
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"devicemsg",
|
||||
"listmedia",
|
||||
@@ -466,7 +467,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.20.0"
|
||||
version = "2.16.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,7 +19,6 @@ 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,12 +92,6 @@ 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:
|
||||
@@ -114,12 +108,7 @@ def _run_cli(
|
||||
configure_thread = Thread(
|
||||
target=client.configure,
|
||||
daemon=True,
|
||||
kwargs={
|
||||
"email": args.email,
|
||||
"password": args.password,
|
||||
"displayname": args.displayname,
|
||||
"selfavatar": args.avatar,
|
||||
},
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
@@ -160,6 +160,7 @@ class ViewType(str, Enum):
|
||||
VOICE = "Voice"
|
||||
VIDEO = "Video"
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
@@ -278,3 +279,11 @@ class SocketSecurity(IntEnum):
|
||||
SSL = 1
|
||||
STARTTLS = 2
|
||||
PLAIN = 3
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
JITSI = 2
|
||||
|
||||
@@ -28,7 +28,9 @@ class ACFactory:
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
"""Create a new unconfigured account."""
|
||||
return self.deltachat.add_account()
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
"""Create a new unconfigured bot."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.20.0"
|
||||
"version": "2.16.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,7 +587,6 @@
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.20.0"
|
||||
version = "2.16.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -435,6 +435,10 @@ class Message:
|
||||
"""return True if it's a video message."""
|
||||
return self._view_type == const.DC_MSG_VIDEO
|
||||
|
||||
def is_videochat_invitation(self):
|
||||
"""return True if it's a videochat invitation message."""
|
||||
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
|
||||
|
||||
def is_webxdc(self):
|
||||
"""return True if it's a Webxdc message."""
|
||||
return self._view_type == const.DC_MSG_WEBXDC
|
||||
@@ -475,6 +479,7 @@ _view_type_mapping = {
|
||||
"video": const.DC_MSG_VIDEO,
|
||||
"file": const.DC_MSG_FILE,
|
||||
"sticker": const.DC_MSG_STICKER,
|
||||
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
|
||||
"webxdc": const.DC_MSG_WEBXDC,
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-10-13
|
||||
2025-10-01
|
||||
@@ -78,7 +78,7 @@ impl Accounts {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
@@ -724,7 +724,8 @@ 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,7 +458,8 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
"Failed to scale image to below {}B.",
|
||||
max_bytes,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
115
src/calls.rs
115
src/calls.rs
@@ -46,9 +46,9 @@ const STUN_PORT: u16 = 3478;
|
||||
///
|
||||
/// It is used to distinguish "ended" calls
|
||||
/// that are rejected by us from the calls
|
||||
/// canceled by the other side
|
||||
/// cancelled by the other side
|
||||
/// immediately after ringing started.
|
||||
const CALL_CANCELED_TIMESTAMP: Param = Param::Arg2;
|
||||
const CALL_CANCELLED_TIMESTAMP: Param = Param::Arg2;
|
||||
|
||||
/// Information about the status of a call.
|
||||
#[derive(Debug, Default)]
|
||||
@@ -96,7 +96,7 @@ impl CallInfo {
|
||||
let duration = match minutes {
|
||||
0 => "<1 minute".to_string(),
|
||||
1 => "1 minute".to_string(),
|
||||
n => format!("{n} minutes"),
|
||||
n => format!("{} minutes", n),
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
@@ -123,14 +123,14 @@ impl CallInfo {
|
||||
}
|
||||
|
||||
/// Returns true if the call is missed
|
||||
/// because the caller canceled it
|
||||
/// because the caller cancelled it
|
||||
/// explicitly before ringing stopped.
|
||||
///
|
||||
/// For outgoing calls this means
|
||||
/// the receiver has rejected the call
|
||||
/// explicitly.
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.msg.param.exists(CALL_CANCELED_TIMESTAMP)
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.msg.param.exists(CALL_CANCELLED_TIMESTAMP)
|
||||
}
|
||||
|
||||
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
|
||||
@@ -139,17 +139,17 @@ impl CallInfo {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Explicitly mark the call as canceled.
|
||||
/// Explicitly mark the call as cancelled.
|
||||
///
|
||||
/// For incoming calls this should be called
|
||||
/// when "call ended" message is received
|
||||
/// from the caller before we picked up the call.
|
||||
/// In this case the call becomes "missed" early
|
||||
/// before the ringing timeout.
|
||||
async fn mark_as_canceled(&mut self, context: &Context) -> Result<()> {
|
||||
async fn mark_as_cancelled(&mut self, context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
|
||||
self.msg.param.set_i64(CALL_CANCELED_TIMESTAMP, now);
|
||||
self.msg.param.set_i64(CALL_CANCELLED_TIMESTAMP, now);
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -183,11 +183,7 @@ impl Context {
|
||||
place_call_info: String,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Can only place calls in 1:1 chats"
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
ensure!(chat.typ == Chattype::Single && !chat.is_self_talk());
|
||||
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
@@ -213,9 +209,7 @@ impl Context {
|
||||
call_id: MsgId,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
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")
|
||||
})?;
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
ensure!(call.is_incoming());
|
||||
if call.is_accepted() || call.is_ended() {
|
||||
info!(self, "Call already accepted/ended");
|
||||
@@ -250,9 +244,7 @@ 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?.with_context(|| {
|
||||
format!("end_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
if call.is_ended() {
|
||||
info!(self, "Call already ended");
|
||||
return Ok(());
|
||||
@@ -263,8 +255,8 @@ impl Context {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
call.mark_as_cancelled(self).await?;
|
||||
call.update_text(self, "Cancelled call").await?;
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
@@ -295,20 +287,14 @@ impl Context {
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).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(());
|
||||
};
|
||||
let mut call = context.load_call_by_id(call_id).await?;
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
call.mark_as_cancelled(&context).await?;
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
call.update_text(&context, "Canceled call").await?;
|
||||
call.update_text(&context, "Cancelled call").await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
@@ -326,10 +312,7 @@ impl Context {
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
if mime_message.is_call() {
|
||||
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(());
|
||||
};
|
||||
let call = self.load_call_by_id(call_id).await?;
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
@@ -365,11 +348,7 @@ impl Context {
|
||||
} else {
|
||||
match mime_message.is_system_message {
|
||||
SystemMessage::CallAccepted => {
|
||||
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(());
|
||||
};
|
||||
|
||||
let mut call = self.load_call_by_id(call_id).await?;
|
||||
if call.is_ended() || call.is_accepted() {
|
||||
info!(self, "CallAccepted received for accepted/ended call");
|
||||
return Ok(());
|
||||
@@ -394,11 +373,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
SystemMessage::CallEnded => {
|
||||
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(());
|
||||
};
|
||||
|
||||
let mut call = self.load_call_by_id(call_id).await?;
|
||||
if call.is_ended() {
|
||||
// may happen eg. if a a message is missed
|
||||
info!(self, "CallEnded received for ended call");
|
||||
@@ -411,14 +386,14 @@ impl Context {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.mark_as_cancelled(self).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
call.mark_as_cancelled(self).await?;
|
||||
call.update_text(self, "Cancelled call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
@@ -442,26 +417,15 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Loads information about the call given its ID.
|
||||
///
|
||||
/// 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>> {
|
||||
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<CallInfo> {
|
||||
let call = Message::load_from_db(self, call_id).await?;
|
||||
Ok(self.load_call_by_message(call))
|
||||
self.load_call_by_message(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;
|
||||
}
|
||||
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
|
||||
ensure!(call.viewtype == Viewtype::Call);
|
||||
|
||||
Some(CallInfo {
|
||||
Ok(CallInfo {
|
||||
place_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
@@ -519,23 +483,18 @@ pub enum CallState {
|
||||
/// that was declined before the timeout.
|
||||
Declined,
|
||||
|
||||
/// Outgoing call that has been canceled on our side
|
||||
/// Outgoing call that has been cancelled on our side
|
||||
/// before receiving a response.
|
||||
///
|
||||
/// Incoming calls cannot be canceled,
|
||||
/// on the receiver side canceled calls
|
||||
/// Incoming calls cannot be cancelled,
|
||||
/// on the receiver side cancelled calls
|
||||
/// usually result in missed calls.
|
||||
Canceled,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// 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?
|
||||
.with_context(|| format!("{msg_id} is not a call message"))?;
|
||||
let call = context.load_call_by_id(msg_id).await?;
|
||||
let state = if call.is_incoming() {
|
||||
if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
@@ -545,8 +504,8 @@ pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
// Call was explicitly canceled
|
||||
} else if call.is_cancelled() {
|
||||
// Call was explicitly cancelled
|
||||
// by the caller before we picked it up.
|
||||
CallState::Missed
|
||||
} else if call.is_ended() {
|
||||
@@ -564,8 +523,8 @@ pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
CallState::Canceled
|
||||
} else if call.is_cancelled() {
|
||||
CallState::Cancelled
|
||||
} else if call.is_ended() || call.is_stale() {
|
||||
CallState::Declined
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
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 {
|
||||
@@ -55,10 +52,7 @@ 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?
|
||||
.expect("m should be a call message");
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
@@ -76,10 +70,7 @@ 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?
|
||||
.expect("IncomingCall event should refer to a call message");
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
@@ -119,10 +110,7 @@ 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?
|
||||
.expect("bob_call should be a call message");
|
||||
let info = bob.load_call_by_id(bob_call.id).await?;
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
|
||||
@@ -132,10 +120,7 @@ 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?
|
||||
.expect("bob2_call should be a call message");
|
||||
let info = bob2.load_call_by_id(bob2_call.id).await?;
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
|
||||
|
||||
@@ -154,10 +139,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
accept_call_info: ACCEPT_INFO.to_string()
|
||||
}
|
||||
);
|
||||
let info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
let info = alice.load_call_by_id(alice_call.id).await?;
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
|
||||
@@ -386,7 +368,7 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
|
||||
// Alice changes their mind before Bob picks up
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Canceled call").await?;
|
||||
assert_text(&alice, alice_call.id, "Cancelled call").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -394,18 +376,18 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Canceled
|
||||
CallState::Cancelled
|
||||
);
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Canceled call").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Cancelled call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Canceled
|
||||
CallState::Cancelled
|
||||
);
|
||||
|
||||
// Bob receives the ending message
|
||||
@@ -416,11 +398,6 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
.await;
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Missed);
|
||||
|
||||
// Test that message summary says it is a missed call.
|
||||
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
|
||||
let summary = bob_call_msg.get_summary(&bob, None).await?;
|
||||
assert_eq!(summary.text, "📞 Missed call");
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
bob2.evtracker
|
||||
@@ -477,20 +454,14 @@ 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?
|
||||
.expect("alice_call should be a call message");
|
||||
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
|
||||
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?
|
||||
.expect("alice_call should be a call message");
|
||||
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
@@ -507,10 +478,7 @@ 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?
|
||||
.expect("alice_call should be a call message");
|
||||
let call_info = alice.load_call_by_id(alice_call.id).await?;
|
||||
call_info.update_text(&alice, "foo bar").await?;
|
||||
|
||||
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
@@ -525,144 +493,3 @@ async fn test_sdp_has_video() {
|
||||
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
|
||||
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
|
||||
}
|
||||
|
||||
/// Tests that calls are forwarded as text messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let alice_msg_id = alice
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
|
||||
.await
|
||||
.context("Failed to place a call")?;
|
||||
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
|
||||
let _alice_sent_call = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_call.viewtype, Viewtype::Call);
|
||||
|
||||
let alice_charlie_chat = alice.create_chat(charlie).await;
|
||||
forward_msgs(alice, &[alice_call.id], alice_charlie_chat.id).await?;
|
||||
let alice_forwarded_call = alice.pop_sent_msg().await;
|
||||
let alice_forwarded_call_msg = alice_forwarded_call.load_from_db().await;
|
||||
assert_eq!(alice_forwarded_call_msg.viewtype, Viewtype::Text);
|
||||
|
||||
let charlie_forwarded_call = charlie.recv_msg(&alice_forwarded_call).await;
|
||||
assert_eq!(charlie_forwarded_call.viewtype, Viewtype::Text);
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
83
src/chat.rs
83
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,7 +702,8 @@ 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
|
||||
@@ -812,7 +813,8 @@ 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?;
|
||||
@@ -2689,7 +2691,10 @@ impl ChatIdBlocked {
|
||||
}
|
||||
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::Call {
|
||||
if msg.viewtype == Viewtype::Text
|
||||
|| msg.viewtype == Viewtype::VideochatInvitation
|
||||
|| msg.viewtype == Viewtype::Call
|
||||
{
|
||||
// the caller should check if the message text is empty
|
||||
} else if msg.viewtype.has_file() {
|
||||
let viewtype_orig = msg.viewtype;
|
||||
@@ -3143,7 +3148,8 @@ 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);
|
||||
@@ -3159,6 +3165,10 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
|
||||
);
|
||||
ensure!(!original_msg.is_info(), "Cannot edit info messages");
|
||||
ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
|
||||
ensure!(
|
||||
original_msg.viewtype != Viewtype::VideochatInvitation,
|
||||
"Cannot edit videochat invitations"
|
||||
);
|
||||
ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls");
|
||||
ensure!(
|
||||
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
|
||||
@@ -3207,6 +3217,34 @@ pub(crate) async fn save_text_edit_to_db(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends invitation to a videochat.
|
||||
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"video chat invitation cannot be sent to special chat: {}",
|
||||
chat_id
|
||||
);
|
||||
|
||||
let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await? {
|
||||
if !instance.is_empty() {
|
||||
instance
|
||||
} else {
|
||||
bail!("webrtc_instance is empty");
|
||||
}
|
||||
} else {
|
||||
bail!("webrtc_instance not set");
|
||||
};
|
||||
|
||||
let instance = Message::create_webrtc_instance(&instance, &create_id());
|
||||
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
msg.text =
|
||||
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
|
||||
.await;
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
async fn donation_request_maybe(context: &Context) -> Result<()> {
|
||||
let secs_between_checks = 30 * 24 * 60 * 60;
|
||||
let now = time();
|
||||
@@ -3911,11 +3949,13 @@ 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,
|
||||
"{chat_id} is not a group/broadcast where one can add members"
|
||||
"{} is not a group/broadcast where one can add members",
|
||||
chat_id
|
||||
);
|
||||
ensure!(
|
||||
Contact::real_exists_by_id(context, contact_id).await? || contact_id == ContactId::SELF,
|
||||
"invalid contact_id {contact_id} for adding to group"
|
||||
"invalid contact_id {} for adding to group",
|
||||
contact_id
|
||||
);
|
||||
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
||||
ensure!(
|
||||
@@ -4128,7 +4168,8 @@ 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,
|
||||
@@ -4142,7 +4183,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;
|
||||
|
||||
@@ -4166,7 +4207,7 @@ pub async fn remove_contact_from_chat(
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
let addr = contact.get_addr();
|
||||
|
||||
let res = send_member_removal_msg(context, &chat, contact_id, addr).await;
|
||||
let res = send_member_removal_msg(context, chat_id, contact_id, addr).await;
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
res?;
|
||||
@@ -4190,7 +4231,7 @@ pub async fn remove_contact_from_chat(
|
||||
// For incoming broadcast channels, it's not possible to remove members,
|
||||
// but it's possible to leave:
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
send_member_removal_msg(context, &chat, contact_id, &self_addr).await?;
|
||||
send_member_removal_msg(context, chat_id, contact_id, &self_addr).await?;
|
||||
} else {
|
||||
bail!("Cannot remove members from non-group chats.");
|
||||
}
|
||||
@@ -4200,18 +4241,14 @@ pub async fn remove_contact_from_chat(
|
||||
|
||||
async fn send_member_removal_msg(
|
||||
context: &Context,
|
||||
chat: &Chat,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
addr: &str,
|
||||
) -> Result<MsgId> {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
if chat.typ == Chattype::InBroadcast {
|
||||
msg.text = stock_str::msg_you_left_broadcast(context).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
}
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
|
||||
}
|
||||
@@ -4221,7 +4258,7 @@ async fn send_member_removal_msg(
|
||||
msg.param
|
||||
.set(Param::ContactAddedRemoved, contact_id.to_u32());
|
||||
|
||||
send_msg(context, chat.id, &mut msg).await
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
|
||||
@@ -4362,7 +4399,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() {
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.emit_msgs_changed(chat_id, msg.id);
|
||||
}
|
||||
@@ -4384,7 +4421,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());
|
||||
@@ -4409,10 +4446,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
}
|
||||
|
||||
if msg.get_viewtype() == Viewtype::Call {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
}
|
||||
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
@@ -4422,8 +4455,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.param.remove(Param::IsEdited);
|
||||
msg.param.remove(Param::WebrtcRoom);
|
||||
msg.param.remove(Param::WebrtcAccepted);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn test_chat_info() {
|
||||
"archived": false,
|
||||
"param": "",
|
||||
"is_sending_locations": false,
|
||||
"color": 29381,
|
||||
"color": 29377,
|
||||
"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, 0x6239dc);
|
||||
assert_eq!(color1, 0x613dd7);
|
||||
|
||||
// upper-/lowercase makes a difference for the colors, these are different groups
|
||||
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
||||
@@ -3026,7 +3026,7 @@ async fn test_leave_broadcast() -> Result<()> {
|
||||
}
|
||||
|
||||
/// Tests that if Bob leaves a broadcast channel with one device,
|
||||
/// the other device shows a correct info message "You left the channel.".
|
||||
/// the other device shows a correct info message "You left.".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3061,7 +3061,10 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
|
||||
assert!(rcvd.is_info());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert_eq!(rcvd.text, stock_str::msg_you_left_broadcast(bob1).await);
|
||||
assert_eq!(
|
||||
rcvd.text,
|
||||
stock_str::msg_group_left_local(bob1, ContactId::SELF).await
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4568,6 +4571,17 @@ async fn test_cannot_send_edit_request() -> Result<()> {
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(alice, chat_id).await?;
|
||||
assert!(
|
||||
send_edit_request(alice, msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
|
||||
@@ -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.23;
|
||||
let chroma = 0.22;
|
||||
let angle = str_to_angle(s);
|
||||
let oklch = Oklch::new(lightness, chroma, angle);
|
||||
let rgb = oklch.to_rgb(TransferFunction::Srgb);
|
||||
|
||||
@@ -346,6 +346,9 @@ pub enum Config {
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
@@ -410,6 +413,16 @@ pub enum Config {
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Deprecated 2025-07. Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// to 1 if it supports verified 1:1 chats.
|
||||
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
|
||||
/// and when the key changes, an info message is posted into the chat.
|
||||
/// 0=Nothing else happens when the key changes.
|
||||
/// 1=After the key changed, `can_send()` returns false
|
||||
/// until `chat_id.accept()` is called.
|
||||
#[strum(props(default = "0"))]
|
||||
VerifiedOneOnOneChats,
|
||||
|
||||
/// Row ID of the key in the `keypairs` table
|
||||
/// used for signatures, encryption to self and included in `Autocrypt` header.
|
||||
KeyId,
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Context {
|
||||
|
||||
let res = self
|
||||
.inner_configure(param)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
@@ -106,7 +106,7 @@ fn parse_server<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event.xml_content().unwrap_or_default().trim().to_owned();
|
||||
let val = event.unescape().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.xml_content().unwrap_or_default();
|
||||
let val = e.unescape().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.xml_content().unwrap_or_default();
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
|
||||
@@ -60,6 +60,23 @@ pub enum MediaQuality {
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
/// Unknown type.
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
|
||||
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
|
||||
BasicWebrtc = 1,
|
||||
|
||||
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
|
||||
Jitsi = 2,
|
||||
}
|
||||
|
||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
||||
@@ -81,7 +98,6 @@ pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
// reference is the release date.
|
||||
// as not all system get speedy updates,
|
||||
// do not use too small value that will annoy users checking for nonexistent updates.
|
||||
// "90 days" has proven to be too short at some point (user were informed but there was no update)
|
||||
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 183;
|
||||
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
@@ -291,4 +307,16 @@ mod tests {
|
||||
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
|
||||
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_videochattype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::default());
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
VideochatType::BasicWebrtc,
|
||||
VideochatType::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1582,8 +1582,6 @@ 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())
|
||||
}
|
||||
@@ -1744,7 +1742,8 @@ 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,7 +4,6 @@ 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]
|
||||
@@ -760,7 +759,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, 0x4844e2);
|
||||
assert_eq!(color1, 0x4947dc);
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
||||
@@ -774,20 +773,6 @@ 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();
|
||||
|
||||
@@ -972,6 +972,12 @@ impl Context {
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
res.insert(
|
||||
"webrtc_instance",
|
||||
self.get_config(Config::WebrtcInstance)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"media_quality",
|
||||
self.get_config_int(Config::MediaQuality).await?.to_string(),
|
||||
@@ -1048,6 +1054,12 @@ impl Context {
|
||||
"gossip_period",
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats", // deprecated 2025-07
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::sync::LazyLock;
|
||||
|
||||
use quick_xml::{
|
||||
Reader,
|
||||
errors::Error as QuickXmlError,
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
};
|
||||
|
||||
@@ -133,7 +132,6 @@ 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) {
|
||||
@@ -142,9 +140,16 @@ 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)) => {
|
||||
str_cb(&String::from_utf8_lossy(&e as &[_]), &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::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
@@ -154,33 +159,6 @@ 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 {}: {:?}",
|
||||
@@ -198,36 +176,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();
|
||||
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 += " ";
|
||||
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();
|
||||
}
|
||||
|
||||
*buf += event_str.trim_start();
|
||||
} else if add_text == AddText::YesPreserveLineEnds {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(event_str, "\n").as_ref();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,9 +283,8 @@ 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, TestContextManager};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -544,43 +543,4 @@ 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,10 +376,6 @@ 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,
|
||||
@@ -399,11 +395,7 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for ephemeral handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
@@ -445,11 +437,7 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for delete-old handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|
||||
@@ -119,11 +119,6 @@ pub enum HeaderDef {
|
||||
AuthenticationResults,
|
||||
|
||||
/// Node address from iroh where direct addresses have been removed.
|
||||
///
|
||||
/// The node address sent in this header must have
|
||||
/// a non-null relay URL as contacting home relay
|
||||
/// is the only way to reach the node without
|
||||
/// direct addresses and global discovery.
|
||||
IrohNodeAddr,
|
||||
|
||||
/// Advertised gossip topic for one webxdc.
|
||||
|
||||
44
src/imap.rs
44
src/imap.rs
@@ -566,38 +566,10 @@ impl Imap {
|
||||
}
|
||||
session.new_mail = false;
|
||||
|
||||
let mut read_cnt = 0;
|
||||
loop {
|
||||
let (n, fetch_more) = self
|
||||
.fetch_new_msg_batch(context, session, folder, folder_meaning)
|
||||
.await?;
|
||||
read_cnt += n;
|
||||
if !fetch_more {
|
||||
return Ok(read_cnt > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns number of messages processed and whether the function should be called again.
|
||||
async fn fetch_new_msg_batch(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
folder: &str,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<(usize, bool)> {
|
||||
let uid_validity = get_uidvalidity(context, folder).await?;
|
||||
let old_uid_next = get_uid_next(context, folder).await?;
|
||||
info!(
|
||||
context,
|
||||
"fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
|
||||
);
|
||||
|
||||
let uids_to_prefetch = 500;
|
||||
let msgs = session
|
||||
.prefetch(old_uid_next, uids_to_prefetch)
|
||||
.await
|
||||
.context("prefetch")?;
|
||||
let msgs = session.prefetch(old_uid_next).await.context("prefetch")?;
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
let download_limit = context.download_limit().await?;
|
||||
@@ -757,8 +729,7 @@ impl Imap {
|
||||
largest_uid_fetched
|
||||
};
|
||||
|
||||
let actually_download_messages_future = async {
|
||||
let sender = sender;
|
||||
let actually_download_messages_future = async move {
|
||||
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
|
||||
let mut fetch_partially = false;
|
||||
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
|
||||
@@ -793,17 +764,14 @@ impl Imap {
|
||||
// if the message has arrived after selecting mailbox
|
||||
// and determining its UIDNEXT and before prefetch.
|
||||
let mut new_uid_next = largest_uid_fetched + 1;
|
||||
let fetch_more = fetch_res.is_ok() && {
|
||||
let prefetch_uid_next = old_uid_next + uids_to_prefetch;
|
||||
if fetch_res.is_ok() {
|
||||
// If we have successfully fetched all messages we planned during prefetch,
|
||||
// then we have covered at least the range between old UIDNEXT
|
||||
// and UIDNEXT of the mailbox at the time of selecting it.
|
||||
new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
|
||||
new_uid_next = max(new_uid_next, mailbox_uid_next);
|
||||
|
||||
new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
|
||||
|
||||
prefetch_uid_next < mailbox_uid_next
|
||||
};
|
||||
}
|
||||
if new_uid_next > old_uid_next {
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
}
|
||||
@@ -820,7 +788,7 @@ impl Imap {
|
||||
// establish a new session if this one is broken.
|
||||
fetch_res?;
|
||||
|
||||
Ok((read_cnt, fetch_more))
|
||||
Ok(read_cnt > 0)
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
|
||||
@@ -110,16 +110,14 @@ impl Session {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
|
||||
/// order of ascending delivery time to the server (INTERNALDATE).
|
||||
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
|
||||
/// in the order of ascending delivery time to the server (INTERNALDATE).
|
||||
pub(crate) async fn prefetch(
|
||||
&mut self,
|
||||
uid_next: u32,
|
||||
n_uids: u32,
|
||||
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
let uid_last = uid_next.saturating_add(n_uids - 1);
|
||||
// fetch messages with larger UID than the last one seen
|
||||
let set = format!("{uid_next}:{uid_last}");
|
||||
let set = format!("{uid_next}:*");
|
||||
let mut list = self
|
||||
.uid_fetch(set, PREFETCH_FLAGS)
|
||||
.await
|
||||
@@ -128,7 +126,16 @@ impl Session {
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
// If the mailbox is not empty, results always include
|
||||
// at least one UID, even if last_seen_uid+1 is past
|
||||
// the last UID in the mailbox. It happens because
|
||||
// uid:* is interpreted the same way as *:uid.
|
||||
// See <https://tools.ietf.org/html/rfc3501#page-61> for
|
||||
// standard reference. Therefore, sometimes we receive
|
||||
// already seen messages and have to filter them out.
|
||||
if msg_uid >= uid_next {
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
107
src/imex.rs
107
src/imex.rs
@@ -928,56 +928,75 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_export_and_import_backup() -> Result<()> {
|
||||
let backup_dir = tempfile::tempdir().unwrap();
|
||||
for set_verified_oneonone_chats in [true, false] {
|
||||
let backup_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let context1 = TestContext::new_alice().await;
|
||||
assert!(context1.is_configured().await?);
|
||||
let context1 = TestContext::new_alice().await;
|
||||
assert!(context1.is_configured().await?);
|
||||
if set_verified_oneonone_chats {
|
||||
context1
|
||||
.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let context2 = TestContext::new().await;
|
||||
assert!(!context2.is_configured().await?);
|
||||
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
|
||||
let context2 = TestContext::new().await;
|
||||
assert!(!context2.is_configured().await?);
|
||||
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
|
||||
|
||||
// export from context1
|
||||
assert!(
|
||||
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
|
||||
// export from context1
|
||||
assert!(
|
||||
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
// import to context2
|
||||
let backup = has_backup(&context2, backup_dir.path()).await?;
|
||||
|
||||
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
|
||||
assert!(
|
||||
imex(
|
||||
&context2,
|
||||
ImexMode::ImportBackup,
|
||||
backup.as_ref(),
|
||||
Some("foobar".to_string())
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// import to context2
|
||||
let backup = has_backup(&context2, backup_dir.path()).await?;
|
||||
assert!(
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
|
||||
assert!(
|
||||
imex(
|
||||
&context2,
|
||||
ImexMode::ImportBackup,
|
||||
backup.as_ref(),
|
||||
Some("foobar".to_string())
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::Addr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::Addr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
context2
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?,
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
context1
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?,
|
||||
set_verified_oneonone_chats
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ impl BackupProvider {
|
||||
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).race(
|
||||
async {
|
||||
cancel_token.recv().await.ok();
|
||||
Err(format_err!("Backup transfer canceled"))
|
||||
Err(format_err!("Backup transfer cancelled"))
|
||||
}
|
||||
).race(
|
||||
async {
|
||||
@@ -262,12 +262,12 @@ impl BackupProvider {
|
||||
}
|
||||
},
|
||||
_ = cancel_token.recv() => {
|
||||
info!(context, "Backup transfer canceled by the user, stopping accept loop.");
|
||||
info!(context, "Backup transfer cancelled by the user, stopping accept loop.");
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
}
|
||||
_ = drop_token.cancelled() => {
|
||||
info!(context, "Backup transfer canceled by dropping the provider, stopping accept loop.");
|
||||
info!(context, "Backup transfer cancelled by dropping the provider, stopping accept loop.");
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
}
|
||||
@@ -364,7 +364,7 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
let res = get_backup2(context, node_addr, auth_token)
|
||||
.race(async {
|
||||
cancel_token.recv().await.ok();
|
||||
Err(format_err!("Backup reception canceled"))
|
||||
Err(format_err!("Backup reception cancelled"))
|
||||
})
|
||||
.await;
|
||||
if let Err(ref res) = res {
|
||||
|
||||
13
src/key.rs
13
src/key.rs
@@ -15,7 +15,6 @@ 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};
|
||||
@@ -415,11 +414,15 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
|
||||
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
|
||||
(new_key_id,),
|
||||
)?;
|
||||
Ok(new_key_id)
|
||||
Ok(Some(new_key_id))
|
||||
})
|
||||
.await?;
|
||||
context.emit_event(EventType::AccountsItemChanged);
|
||||
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -497,7 +500,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.xml_content().unwrap_or_default();
|
||||
let val = event.unescape().unwrap_or_default();
|
||||
|
||||
let val = val.replace(['\n', '\r', '\t', ' '], "");
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility, send_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL};
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL, VideochatType,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
@@ -490,7 +492,8 @@ 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 {id} from DB"
|
||||
"Can not load special message ID {} from DB",
|
||||
id
|
||||
);
|
||||
let msg = context
|
||||
.sql
|
||||
@@ -565,7 +568,7 @@ impl Message {
|
||||
timestamp_rcvd: row.get("timestamp_rcvd")?,
|
||||
ephemeral_timer: row.get("ephemeral_timer")?,
|
||||
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
|
||||
viewtype: row.get("type").unwrap_or_default(),
|
||||
viewtype: row.get("type")?,
|
||||
state: state.with_mdns(mdn_msg_id.is_some()),
|
||||
download_state: row.get("download_state")?,
|
||||
error: Some(row.get::<_, String>("error")?)
|
||||
@@ -1014,6 +1017,85 @@ impl Message {
|
||||
None
|
||||
}
|
||||
|
||||
// add room to a webrtc_instance as defined by the corresponding config-value;
|
||||
// the result may still be prefixed by the type
|
||||
pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String {
|
||||
let (videochat_type, mut url) = Message::parse_webrtc_instance(instance);
|
||||
|
||||
// make sure, there is a scheme in the url
|
||||
if !url.contains(':') {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
// add/replace room
|
||||
let url = if url.contains("$ROOM") {
|
||||
url.replace("$ROOM", room)
|
||||
} else if url.contains("$NOROOM") {
|
||||
// there are some usecases where a separate room is not needed to use a service
|
||||
// eg. if you let in people manually anyway, see discussion at
|
||||
// <https://support.delta.chat/t/videochat-with-webex/1412/4>.
|
||||
// hacks as hiding the room behind `#` are not reliable, therefore,
|
||||
// these services are supported by adding the string `$NOROOM` to the url.
|
||||
url.replace("$NOROOM", "")
|
||||
} else {
|
||||
// if there nothing that would separate the room, add a slash as a separator;
|
||||
// this way, urls can be given as "https://meet.jit.si" as well as "https://meet.jit.si/"
|
||||
let maybe_slash = if url.ends_with('/')
|
||||
|| url.ends_with('?')
|
||||
|| url.ends_with('#')
|
||||
|| url.ends_with('=')
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"/"
|
||||
};
|
||||
format!("{url}{maybe_slash}{room}")
|
||||
};
|
||||
|
||||
// re-add and normalize type
|
||||
match videochat_type {
|
||||
VideochatType::BasicWebrtc => format!("basicwebrtc:{url}"),
|
||||
VideochatType::Jitsi => format!("jitsi:{url}"),
|
||||
VideochatType::Unknown => url,
|
||||
}
|
||||
}
|
||||
|
||||
/// split a webrtc_instance as defined by the corresponding config-value into a type and a url
|
||||
pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
|
||||
let instance: String = instance.split_whitespace().collect();
|
||||
let mut split = instance.splitn(2, ':');
|
||||
let type_str = split.next().unwrap_or_default().to_lowercase();
|
||||
let url = split.next();
|
||||
match type_str.as_str() {
|
||||
"basicwebrtc" => (
|
||||
VideochatType::BasicWebrtc,
|
||||
url.unwrap_or_default().to_string(),
|
||||
),
|
||||
"jitsi" => (VideochatType::Jitsi, url.unwrap_or_default().to_string()),
|
||||
_ => (VideochatType::Unknown, instance.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns videochat URL if the message is a videochat invitation.
|
||||
pub fn get_videochat_url(&self) -> Option<String> {
|
||||
if self.viewtype == Viewtype::VideochatInvitation {
|
||||
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
|
||||
return Some(Message::parse_webrtc_instance(instance).1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns videochat type if the message is a videochat invitation.
|
||||
pub fn get_videochat_type(&self) -> Option<VideochatType> {
|
||||
if self.viewtype == Viewtype::VideochatInvitation {
|
||||
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
|
||||
return Some(Message::parse_webrtc_instance(instance).0);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Sets or unsets message text.
|
||||
pub fn set_text(&mut self, text: String) {
|
||||
self.text = text;
|
||||
@@ -2195,6 +2277,9 @@ pub enum Viewtype {
|
||||
/// and retrieved via dc_msg_get_file().
|
||||
File = 60,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation = 70,
|
||||
|
||||
/// Message is an incoming or outgoing call.
|
||||
Call = 71,
|
||||
|
||||
@@ -2220,6 +2305,7 @@ impl Viewtype {
|
||||
Viewtype::Voice => true,
|
||||
Viewtype::Video => true,
|
||||
Viewtype::File => true,
|
||||
Viewtype::VideochatInvitation => false,
|
||||
Viewtype::Call => false,
|
||||
Viewtype::Webxdc => true,
|
||||
Viewtype::Vcard => true,
|
||||
|
||||
@@ -25,6 +25,82 @@ fn test_guess_msgtype_from_suffix() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "https://foo/bar");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "url");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
|
||||
assert_eq!(webrtc_type, VideochatType::Unknown);
|
||||
assert_eq!(url, "https://foo/bar?key=val#key=val");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo");
|
||||
assert_eq!(webrtc_type, VideochatType::Jitsi);
|
||||
assert_eq!(url, "https://j.si/foo");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123");
|
||||
assert_eq!(instance, "https://meet.jit.si/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si", "456");
|
||||
assert_eq!(instance, "https://meet.jit.si/456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("meet.jit.si", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo?", "123");
|
||||
assert_eq!(instance, "https://bla.foo?123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456");
|
||||
assert_eq!(instance, "jitsi:https://bla.foo#456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=", "789");
|
||||
assert_eq!(instance, "https://bla.foo#room=789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://bla.foo#room", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234");
|
||||
assert_eq!(instance, "https://bla.foo#room=234&after=cont");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" meet.jit .si ", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab");
|
||||
assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance_noroom() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?a=b");
|
||||
|
||||
// $ROOM has a higher precedence
|
||||
let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?$NOROOM=123");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -572,6 +648,10 @@ fn test_viewtype_values() {
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
|
||||
}
|
||||
@@ -747,21 +827,3 @@ async fn test_send_empty_file() -> Result<()> {
|
||||
assert_eq!(bob_received_msg.get_viewtype(), Viewtype::File);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that viewtype 70
|
||||
/// which previously corresponded to videochat invitations,
|
||||
/// is loaded as unknown viewtype without errors.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_unknown_viewtype() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let msg_id = tcm.send_recv(alice, bob, "Hello!").await.id;
|
||||
bob.sql
|
||||
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg_id,))
|
||||
.await?;
|
||||
let bob_msg = Message::load_from_db(bob, msg_id).await?;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Unknown);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -419,7 +419,10 @@ 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.
|
||||
@@ -1016,7 +1019,6 @@ impl MimeFactory {
|
||||
| "in-reply-to"
|
||||
| "references"
|
||||
| "auto-submitted"
|
||||
| "chat-version"
|
||||
| "autocrypt-setup-message" => {
|
||||
unprotected_headers.push(header.clone());
|
||||
}
|
||||
@@ -1519,19 +1521,16 @@ impl MimeFactory {
|
||||
));
|
||||
}
|
||||
SystemMessage::IrohNodeAddr => {
|
||||
let node_addr = context
|
||||
.get_or_try_init_peer_channel()
|
||||
.await?
|
||||
.get_node_addr()
|
||||
.await?;
|
||||
|
||||
// We should not send `null` as relay URL
|
||||
// as this is the only way to reach the node.
|
||||
debug_assert!(node_addr.relay_url().is_some());
|
||||
headers.push((
|
||||
HeaderDef::IrohNodeAddr.into(),
|
||||
mail_builder::headers::text::Text::new(serde_json::to_string(&node_addr)?)
|
||||
.into(),
|
||||
mail_builder::headers::text::Text::new(serde_json::to_string(
|
||||
&context
|
||||
.get_or_try_init_peer_channel()
|
||||
.await?
|
||||
.get_node_addr()
|
||||
.await?,
|
||||
)?)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::CallAccepted => {
|
||||
@@ -1565,6 +1564,11 @@ impl MimeFactory {
|
||||
"Chat-Content",
|
||||
mail_builder::headers::raw::Raw::new("sticker").into(),
|
||||
));
|
||||
} else if msg.viewtype == Viewtype::VideochatInvitation {
|
||||
headers.push((
|
||||
"Chat-Content",
|
||||
mail_builder::headers::raw::Raw::new("videochat-invitation").into(),
|
||||
));
|
||||
} else if msg.viewtype == Viewtype::Call {
|
||||
headers.push((
|
||||
"Chat-Content",
|
||||
|
||||
@@ -732,10 +732,12 @@ impl MimeMessage {
|
||||
.map(|s| s.to_string());
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
if let Some(room) = room {
|
||||
if content == "call" {
|
||||
part.typ = Viewtype::Call;
|
||||
part.param.set(Param::WebrtcRoom, room);
|
||||
if content == "videochat-invitation" {
|
||||
part.typ = Viewtype::VideochatInvitation;
|
||||
} else if content == "call" {
|
||||
part.typ = Viewtype::Call
|
||||
}
|
||||
part.param.set(Param::WebrtcRoom, room);
|
||||
} else if let Some(accepted) = accepted {
|
||||
part.param.set(Param::WebrtcAccepted, accepted);
|
||||
}
|
||||
@@ -763,7 +765,10 @@ impl MimeMessage {
|
||||
| Viewtype::Vcard
|
||||
| Viewtype::File
|
||||
| Viewtype::Webxdc => true,
|
||||
Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
|
||||
Viewtype::Unknown
|
||||
| Viewtype::Text
|
||||
| Viewtype::VideochatInvitation
|
||||
| Viewtype::Call => false,
|
||||
})
|
||||
{
|
||||
let mut parts = std::mem::take(&mut self.parts);
|
||||
@@ -2080,7 +2085,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -476,10 +476,6 @@ async fn test_mimeparser_with_avatars() {
|
||||
assert!(mimeparser.group_avatar.unwrap().is_change());
|
||||
}
|
||||
|
||||
/// Tests that video chat invitations that are not supported anymore
|
||||
/// are displayed as text messages.
|
||||
///
|
||||
/// User can still click on the link manually.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mimeparser_with_videochat() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -487,8 +483,14 @@ async fn test_mimeparser_with_videochat() {
|
||||
let raw = include_bytes!("../../test-data/message/videochat_invitation.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert_eq!(mimeparser.parts[0].param.get(Param::WebrtcRoom), None);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0]
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default(),
|
||||
"https://example.org/p2p/?roomname=6HiduoAn4xN"
|
||||
);
|
||||
assert!(
|
||||
mimeparser.parts[0]
|
||||
.msg
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,14 +185,9 @@ impl Iroh {
|
||||
}
|
||||
|
||||
/// Get the iroh [NodeAddr] without direct IP addresses.
|
||||
///
|
||||
/// The address is guaranteed to have home relay URL set
|
||||
/// as it is the only way to reach the node
|
||||
/// without global discovery mechanisms.
|
||||
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
|
||||
let mut addr = self.router.endpoint().node_addr().await?;
|
||||
addr.direct_addresses = BTreeSet::new();
|
||||
debug_assert!(addr.relay_url().is_some());
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
|
||||
54
src/qr.rs
54
src/qr.rs
@@ -16,6 +16,7 @@ use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::Fingerprint;
|
||||
use crate::message::Message;
|
||||
use crate::net::http::post_empty;
|
||||
use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig};
|
||||
use crate::token;
|
||||
@@ -26,6 +27,7 @@ const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
|
||||
const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
|
||||
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
|
||||
const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
|
||||
const MAILTO_SCHEME: &str = "mailto:";
|
||||
const MATMSG_SCHEME: &str = "MATMSG:";
|
||||
@@ -120,6 +122,15 @@ pub enum Qr {
|
||||
/// The QR code is a backup, but it is too new. The user has to update its Delta Chat.
|
||||
BackupTooNew {},
|
||||
|
||||
/// Ask the user if they want to use the given service for video chats.
|
||||
WebrtcInstance {
|
||||
/// Server domain name.
|
||||
domain: String,
|
||||
|
||||
/// URL pattern for video chat rooms.
|
||||
instance_pattern: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to use the given proxy.
|
||||
///
|
||||
/// Note that HTTP(S) URLs without a path
|
||||
@@ -283,6 +294,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
decode_account(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
|
||||
dclogin_scheme::decode_login(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
|
||||
decode_webrtc_instance(context, qr)?
|
||||
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
|
||||
decode_tg_socks_proxy(context, qr)?
|
||||
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
|
||||
@@ -408,7 +421,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 +445,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
|
||||
@@ -560,6 +573,28 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
}
|
||||
}
|
||||
|
||||
/// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM`
|
||||
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCWEBRTC_SCHEME.len()..)
|
||||
.context("Invalid DCWEBRTC payload")?;
|
||||
|
||||
let (_type, url) = Message::parse_webrtc_instance(payload);
|
||||
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
|
||||
|
||||
if url.scheme() == "http" || url.scheme() == "https" {
|
||||
Ok(Qr::WebrtcInstance {
|
||||
domain: url
|
||||
.host_str()
|
||||
.context("can't extract WebRTC instance domain")?
|
||||
.to_string(),
|
||||
instance_pattern: payload.to_string(),
|
||||
})
|
||||
} else {
|
||||
bail!("Bad URL scheme for WebRTC instance: {:?}", url.scheme());
|
||||
}
|
||||
}
|
||||
|
||||
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
|
||||
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
|
||||
@@ -581,7 +616,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,7 +719,10 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -694,6 +732,14 @@ pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(
|
||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
match check_qr(context, qr).await? {
|
||||
Qr::Account { .. } => set_account_from_qr(context, qr).await?,
|
||||
Qr::WebrtcInstance {
|
||||
domain: _,
|
||||
instance_pattern,
|
||||
} => {
|
||||
context
|
||||
.set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
|
||||
.await?;
|
||||
}
|
||||
Qr::Proxy { url, .. } => {
|
||||
let old_proxy_url_value = context
|
||||
.get_config(Config::ProxyUrl)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -712,6 +712,32 @@ async fn test_decode_account() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_webrtc_instance() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::WebrtcInstance {
|
||||
domain: "basicurl.com".to_string(),
|
||||
instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with mixcased "dcWebRTC:" uri scheme
|
||||
let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::WebrtcInstance {
|
||||
domain: "example.org".to_string(),
|
||||
instance_pattern: "https://example.org/".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -794,6 +820,34 @@ async fn test_decode_account_bad_scheme() {
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
||||
assert!(res.is_err());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||
"https://example.org/"
|
||||
);
|
||||
|
||||
let res =
|
||||
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_proxy_config_from_qr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # QR code generation module.
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use qrcodegen::{QrCode, QrCodeEcc};
|
||||
|
||||
@@ -108,18 +108,8 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
|
||||
None => None,
|
||||
};
|
||||
|
||||
let qrcode_description = match chat.typ {
|
||||
crate::constants::Chattype::Group => {
|
||||
stock_str::secure_join_group_qr_description(context, &chat).await
|
||||
}
|
||||
crate::constants::Chattype::OutBroadcast => {
|
||||
stock_str::secure_join_broadcast_qr_description(context, &chat).await
|
||||
}
|
||||
_ => bail!("Unexpected chat type {}", chat.typ),
|
||||
};
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
&qrcode_description,
|
||||
&stock_str::secure_join_group_qr_description(context, &chat).await,
|
||||
&securejoin::get_securejoin_qr(context, Some(chat_id)).await?,
|
||||
&color_int_to_hex_string(chat.get_color(context).await?),
|
||||
avatar,
|
||||
|
||||
@@ -557,17 +557,13 @@ 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 let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
|
||||
.await?
|
||||
.filter(|msg| msg.download_state() != DownloadState::Done)
|
||||
{
|
||||
replace_chat_id = if 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 {
|
||||
@@ -999,7 +995,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
if is_partial_download.is_none() && mime_parser.is_call() {
|
||||
if mime_parser.is_call() {
|
||||
context
|
||||
.handle_call_msg(insert_msg_id, &mime_parser, from_id)
|
||||
.await?;
|
||||
@@ -1157,9 +1153,8 @@ async fn decide_chat_assignment(
|
||||
{
|
||||
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
|
||||
true
|
||||
} else if is_partial_download.is_none()
|
||||
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded)
|
||||
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded
|
||||
{
|
||||
info!(context, "Call state changed (TRASH).");
|
||||
true
|
||||
@@ -1987,9 +1982,8 @@ async fn add_parts(
|
||||
|
||||
handle_edit_delete(context, mime_parser, from_id).await?;
|
||||
|
||||
if is_partial_download.is_none()
|
||||
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded)
|
||||
if 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) =
|
||||
@@ -2870,6 +2864,15 @@ async fn apply_group_changes(
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
let mut silent = false;
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let chat_contacts =
|
||||
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
|
||||
let is_from_in_chat =
|
||||
@@ -2999,15 +3002,6 @@ async fn apply_group_changes(
|
||||
.await?;
|
||||
} else {
|
||||
let mut new_members: HashSet<ContactId>;
|
||||
// True if a Delta Chat client has explicitly and really added our primary address to an
|
||||
// already existing group.
|
||||
let self_added =
|
||||
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
|
||||
&& !chat_contacts.contains(&ContactId::SELF)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if self_added {
|
||||
new_members = HashSet::from_iter(to_ids_flat.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
@@ -3074,18 +3068,17 @@ async fn apply_group_changes(
|
||||
.collect();
|
||||
|
||||
if let Some(added_id) = added_id {
|
||||
if !added_ids.remove(&added_id) && added_id != ContactId::SELF {
|
||||
// No-op "Member added" message. An exception is self-addition messages because they at
|
||||
// least must be shown when a chat is created on our side.
|
||||
if !added_ids.remove(&added_id) && !self_added {
|
||||
// No-op "Member added" message.
|
||||
//
|
||||
// Trash it.
|
||||
better_msg = Some(String::new());
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
removed_ids.remove(&removed_id);
|
||||
}
|
||||
let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
|
||||
&& new_chat_contacts.contains(&ContactId::SELF)
|
||||
{
|
||||
let group_changes_msgs = if self_added {
|
||||
Vec::new()
|
||||
} else {
|
||||
group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
|
||||
@@ -3539,7 +3532,8 @@ async fn apply_in_broadcast_changes(
|
||||
// The only member added/removed message that is ever sent is "I left.",
|
||||
// so, this is the only case we need to handle here
|
||||
if from_id == ContactId::SELF {
|
||||
better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
|
||||
better_msg
|
||||
.get_or_insert(stock_str::msg_group_left_local(context, ContactId::SELF).await);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -72,6 +72,11 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
}
|
||||
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
||||
};
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
@@ -697,6 +702,11 @@ async fn test_lost_contact_confirm() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(&alice, None).await.unwrap();
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -130,6 +130,12 @@ pub enum StockMessage {
|
||||
#[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,
|
||||
|
||||
@@ -273,7 +279,7 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
|
||||
MsgDelMemberBy = 131,
|
||||
|
||||
#[strum(props(fallback = "You left the group."))]
|
||||
#[strum(props(fallback = "You left."))]
|
||||
MsgYouLeftGroup = 132,
|
||||
|
||||
#[strum(props(fallback = "Group left by %1$s."))]
|
||||
@@ -418,27 +424,6 @@ Help keeping us to keep Delta Chat independent and make it more awesome in the f
|
||||
|
||||
https://delta.chat/donate"))]
|
||||
DonationRequest = 193,
|
||||
|
||||
#[strum(props(fallback = "Outgoing call"))]
|
||||
OutgoingCall = 194,
|
||||
|
||||
#[strum(props(fallback = "Incoming call"))]
|
||||
IncomingCall = 195,
|
||||
|
||||
#[strum(props(fallback = "Declined call"))]
|
||||
DeclinedCall = 196,
|
||||
|
||||
#[strum(props(fallback = "Canceled call"))]
|
||||
CanceledCall = 197,
|
||||
|
||||
#[strum(props(fallback = "Missed call"))]
|
||||
MissedCall = 198,
|
||||
|
||||
#[strum(props(fallback = "You left the channel."))]
|
||||
MsgYouLeftBroadcast = 200,
|
||||
|
||||
#[strum(props(fallback = "Scan to join channel %1$s"))]
|
||||
SecureJoinBrodcastQRDescription = 201,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -711,7 +696,7 @@ pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgILeftGroup).await
|
||||
}
|
||||
|
||||
/// Stock string: `You left the group.` or `Group left by %1$s.`.
|
||||
/// Stock string: `You left.` or `Group left by %1$s.`.
|
||||
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouLeftGroup).await
|
||||
@@ -722,11 +707,6 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `You left the channel.`
|
||||
pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgYouLeftBroadcast).await
|
||||
}
|
||||
|
||||
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
|
||||
pub(crate) async fn msg_reacted(
|
||||
context: &Context,
|
||||
@@ -821,31 +801,6 @@ pub(crate) async fn donation_request(context: &Context) -> String {
|
||||
translated(context, StockMessage::DonationRequest).await
|
||||
}
|
||||
|
||||
/// Stock string: `Outgoing call`.
|
||||
pub(crate) async fn outgoing_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::OutgoingCall).await
|
||||
}
|
||||
|
||||
/// Stock string: `Incoming call`.
|
||||
pub(crate) async fn incoming_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::IncomingCall).await
|
||||
}
|
||||
|
||||
/// Stock string: `Declined call`.
|
||||
pub(crate) async fn declined_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeclinedCall).await
|
||||
}
|
||||
|
||||
/// Stock string: `Canceled call`.
|
||||
pub(crate) async fn canceled_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::CanceledCall).await
|
||||
}
|
||||
|
||||
/// Stock string: `Missed call`.
|
||||
pub(crate) async fn missed_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::MissedCall).await
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
pub(crate) async fn setup_contact_qr_description(
|
||||
context: &Context,
|
||||
@@ -862,20 +817,13 @@ pub(crate) async fn setup_contact_qr_description(
|
||||
.replace1(&name)
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to join group %1$s`.
|
||||
/// Stock string: `Scan to join %1$s`.
|
||||
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinGroupQRDescription)
|
||||
.await
|
||||
.replace1(chat.get_name())
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to join channel %1$s`.
|
||||
pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinBrodcastQRDescription)
|
||||
.await
|
||||
.replace1(chat.get_name())
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
@@ -1053,6 +1001,18 @@ pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: Cont
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Video chat invitation`.
|
||||
pub(crate) async fn videochat_invitation(context: &Context) -> String {
|
||||
translated(context, StockMessage::VideochatInvitation).await
|
||||
}
|
||||
|
||||
/// Stock string: `You are invited to a video chat, click %1$s to join.`.
|
||||
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
|
||||
translated(context, StockMessage::VideochatInviteMsgBody)
|
||||
.await
|
||||
.replace1(url)
|
||||
}
|
||||
|
||||
/// Stock string: `Error:\n\n“%1$s”`.
|
||||
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
|
||||
translated(context, StockMessage::ConfigurationFailed)
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::str;
|
||||
|
||||
use crate::calls::{CallState, call_state};
|
||||
use crate::chat::Chat;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
@@ -211,6 +210,12 @@ impl Message {
|
||||
type_file = self.get_filename();
|
||||
append_text = true
|
||||
}
|
||||
Viewtype::VideochatInvitation => {
|
||||
emoji = None;
|
||||
type_name = Some(stock_str::videochat_invitation(context).await);
|
||||
type_file = None;
|
||||
append_text = false;
|
||||
}
|
||||
Viewtype::Webxdc => {
|
||||
emoji = None;
|
||||
type_name = None;
|
||||
@@ -229,21 +234,11 @@ impl Message {
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Call => {
|
||||
let call_state = call_state(context, self.id)
|
||||
.await
|
||||
.unwrap_or(CallState::Alerting);
|
||||
emoji = Some("📞");
|
||||
type_name = Some(match call_state {
|
||||
CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
|
||||
if self.from_id == ContactId::SELF {
|
||||
stock_str::outgoing_call(context).await
|
||||
} else {
|
||||
stock_str::incoming_call(context).await
|
||||
}
|
||||
}
|
||||
CallState::Missed => stock_str::missed_call(context).await,
|
||||
CallState::Declined => stock_str::declined_call(context).await,
|
||||
CallState::Canceled => stock_str::canceled_call(context).await,
|
||||
type_name = Some(if self.from_id == ContactId::SELF {
|
||||
"Outgoing call".to_string()
|
||||
} else {
|
||||
"Incoming call".to_string()
|
||||
});
|
||||
type_file = None;
|
||||
append_text = false
|
||||
@@ -422,6 +417,13 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
|
||||
.unwrap();
|
||||
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
|
||||
|
||||
let mut msg = Message::new(Viewtype::Vcard);
|
||||
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None).unwrap();
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::context::Context;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{self, DcKey, DcSecretKey, self_fingerprint};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MessageState, MsgId, update_msg_state};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype, update_msg_state};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -1535,7 +1535,7 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
|
||||
let msgtext = msg.get_text();
|
||||
writeln!(
|
||||
buf,
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}",
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}",
|
||||
prefix,
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
@@ -1563,6 +1563,15 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
msg.get_videochat_url().unwrap_or_default(),
|
||||
msg.get_videochat_type().unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
if msg.is_forwarded() {
|
||||
"[FORWARDED]"
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
tcm.execute_securejoin(&alice, &bob).await;
|
||||
|
||||
@@ -88,6 +89,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob, &fiona]).await;
|
||||
|
||||
tcm.execute_securejoin(&alice, &bob).await;
|
||||
tcm.execute_securejoin(&bob, &fiona).await;
|
||||
@@ -149,6 +151,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
drop(fiona);
|
||||
|
||||
let fiona_new = tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[&fiona_new]).await;
|
||||
fiona_new.configure_addr("fiona@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&fiona_new).await?;
|
||||
|
||||
@@ -178,6 +181,7 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob]).await;
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
@@ -202,6 +206,7 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
// A chat with an unknown contact should be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
@@ -241,6 +246,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
|
||||
@@ -355,6 +361,7 @@ async fn test_mdn_doesnt_disable_verification() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
bob.set_config_bool(Config::MdnsEnabled, true).await?;
|
||||
|
||||
// Alice & Bob verify each other
|
||||
@@ -379,6 +386,7 @@ async fn test_outgoing_mua_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
@@ -415,6 +423,7 @@ async fn test_outgoing_encrypted_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[alice]).await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
let chat_id = alice.create_chat(bob).await.id;
|
||||
@@ -440,6 +449,7 @@ async fn test_reply() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
if verified {
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
@@ -482,6 +492,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob_old = &tcm.unconfigured().await;
|
||||
|
||||
enable_verified_oneonone_chats(&[alice, bob_old]).await;
|
||||
bob_old.configure_addr("bob@example.net").await;
|
||||
mark_as_verified(bob_old, alice).await;
|
||||
let chat = bob_old.create_chat(alice).await;
|
||||
@@ -492,6 +503,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[bob]).await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
@@ -523,6 +535,7 @@ async fn test_verify_then_verify_again() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
@@ -533,6 +546,7 @@ async fn test_verify_then_verify_again() -> Result<()> {
|
||||
tcm.section("Bob reinstalls DC");
|
||||
drop(bob);
|
||||
let bob_new = tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[&bob_new]).await;
|
||||
bob_new.configure_addr("bob@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&bob_new).await?;
|
||||
|
||||
@@ -585,6 +599,7 @@ async fn test_verified_member_added_reordering() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob, fiona]).await;
|
||||
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
@@ -636,6 +651,7 @@ async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
|
||||
bob.set_config(Config::Displayname, Some("Bob Smith"))
|
||||
.await?;
|
||||
if verified {
|
||||
enable_verified_oneonone_chats(&[&bob]).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
} else {
|
||||
tcm.send_recv_accept(&alice, &bob, "hi").await;
|
||||
@@ -783,12 +799,7 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
|
||||
|
||||
tcm.section("Charlie receives member added message");
|
||||
charlie.recv_msg(&sent_member_added_msg).await;
|
||||
charlie
|
||||
.golden_test_chat(
|
||||
charlie_received_xdc.chat_id,
|
||||
"verified_chats_editor_reordering",
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -871,3 +882,11 @@ async fn assert_verified(this: &TestContext, other: &TestContext, protected: Pro
|
||||
protected == ProtectionStatus::Protected
|
||||
);
|
||||
}
|
||||
|
||||
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
|
||||
for t in test_contexts {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,8 +437,7 @@ async fn test_maybe_warn_on_outdated() {
|
||||
let t = TestContext::new().await;
|
||||
let timestamp_now: i64 = time();
|
||||
|
||||
// in about 3 months, the app should not be outdated.
|
||||
// "90 days" has proven to be too short at some point - user were informed but there was no update
|
||||
// in about 3 months, the app should not be outdated
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + 90 * 24 * 60 * 60,
|
||||
|
||||
@@ -279,7 +279,7 @@ impl Context {
|
||||
};
|
||||
|
||||
if !valid {
|
||||
bail!("{filename} is not a valid webxdc file");
|
||||
bail!("{} is not a valid webxdc file", filename);
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
@@ -2,7 +2,7 @@ Group#Chat#10: Group chat [3 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
|
||||
Msg#11🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#12🔒: Me (Contact#Contact#Self): You left the group. [INFO] √
|
||||
Msg#12🔒: Me (Contact#Contact#Self): You left. [INFO] √
|
||||
Msg#13🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#14🔒: (Contact#Contact#10): What a silence! [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
Group#Chat#11: Group [3 member(s)] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this group.
|
||||
|
||||
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
|
||||
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
|
||||
Msg#16🔒: (Contact#Contact#11): [FRESH]
|
||||
Msg#18: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#19: info (Contact#Contact#Info): Member bob@example.net added. [NOTICED][INFO]
|
||||
Msg#20🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user