Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
959ca06691 python: fail fast on the tests
Do not waste CI time running the rest of the tests
if CI is not going to be green anyway.
2023-03-22 12:35:27 +00:00
137 changed files with 3217 additions and 7152 deletions

25
.github/mergeable.yml vendored
View File

@@ -1,15 +1,26 @@
version: 2
mergeable:
- when: pull_request.*
name: "Conventional Commits"
name: "Changelog check"
validate:
- do: title
begins_with:
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore', 'cargo', 'build', 'ci', 'docs']
- do: or
validate:
- do: description
must_include:
regex: "#skip-changelog"
- do: and
validate:
- do: dependent
changed:
file: "src/**"
required: ["CHANGELOG.md"]
- do: dependent
changed:
file: "deltachat-ffi/src/**"
required: ["CHANGELOG.md"]
fail:
- do: checks
status: "action_required"
payload:
title: PR title should follow conventional commits
summary: "PR title should follow https://conventionalcommits.org. See https://github.com/deltachat/deltachat-core-rust/blob/master/CONTRIBUTING.md for details."
title: Changelog might need an update
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."

View File

@@ -16,11 +16,11 @@ env:
RUSTFLAGS: -Dwarnings
jobs:
lint_rust:
name: Lint Rust
lint:
name: Rustfmt and Clippy
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.68.2
RUSTUP_TOOLCHAIN: 1.68.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -31,8 +31,6 @@ jobs:
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- name: Check
run: cargo check --workspace --all-targets --all-features
cargo_deny:
name: cargo deny
@@ -66,28 +64,31 @@ jobs:
- name: Rustdoc
run: cargo doc --document-private-items --no-deps
rust_tests:
name: Rust tests
build_and_test:
name: Build and test
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
rust: 1.68.2
rust: 1.68.0
python: 3.9
- os: windows-latest
rust: 1.68.2
- os: macos-latest
rust: 1.68.2
rust: 1.68.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.65.0
# Minimum Supported Rust Version = 1.64.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.65.0
rust: 1.64.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@master
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
@@ -96,176 +97,64 @@ jobs:
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Check
run: cargo check --workspace --bins --examples --tests --benches
- name: Tests
run: cargo test --workspace
- name: Test cargo vendor
run: cargo vendor
c_library:
name: Build C library
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Build C library
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Upload C library
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
retention-days: 1
rpc_server:
name: Build deltachat-rpc-server
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug/deltachat-rpc-server
retention-days: 1
python_lint:
name: Python lint
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install tox
run: pip install tox
- name: Lint Python bindings
working-directory: python
run: tox -e lint
- name: Lint deltachat-rpc-client
working-directory: deltachat-rpc-client
run: tox -e lint
python_tests:
name: Python tests
needs: ["c_library", "python_lint"]
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.11
- os: macos-latest
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
- os: macos-latest
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
- os: ubuntu-latest
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Download libdeltachat.a
uses: actions/download-artifact@v3
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Install tox
if: ${{ matrix.python }}
run: pip install tox
- name: Build C library
if: ${{ matrix.python }}
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Run python tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e mypy,doc,py
run: tox -e lint,mypy,doc,py3
aysnc_python_tests:
name: Async Python tests
needs: ["python_lint", "rpc_server"]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python: 3.11
- os: macos-latest
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
- os: macos-latest
python: pypy3.9
# Minimum Supported Python Version = 3.8
#
# Python 3.7 has at least one known bug related to starting subprocesses
# in asyncio programs: <https://bugs.python.org/issue35621>
- os: ubuntu-latest
python: 3.8
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Install tox
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug
- name: Make deltachat-rpc-server executable
run: chmod +x target/debug/deltachat-rpc-server
- name: Build deltachat-rpc-server
if: ${{ matrix.python }}
run: cargo build -p deltachat-rpc-server
- name: Add deltachat-rpc-server to path
if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: Run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py
run: tox -e py3,lint
- name: Install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: "pypy${{ matrix.python }}"
- name: Run pypy tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e pypy3

View File

@@ -15,7 +15,7 @@ jobs:
# Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility.
build_linux:
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
name: Cross-compile deltachat-rpc-server for x86_64, aarch64 and armv7 Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -30,13 +30,6 @@ jobs:
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload i686 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-i686
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload aarch64 binary
uses: actions/upload-artifact@v3
with:
@@ -84,41 +77,42 @@ jobs:
path: target/${{ matrix.target}}/release/${{ matrix.path }}
if-no-files-found: error
build_macos:
name: Build deltachat-rpc-server for macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add x86_64-apple-darwin
- name: Build
run: cargo build --release --package deltachat-rpc-server --target x86_64-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
if-no-files-found: error
publish:
name: Upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
needs: ["build_linux", "build_windows"]
permissions:
contents: write
runs-on: "ubuntu-latest"
steps:
- name: Download built binaries
- name: Download deltachat-rpc-server-x86_64
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-x86_64"
path: "dist/deltachat-rpc-server-x86_64"
- name: Compose dist/ directory
run: |
mkdir dist
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
done
- name: Download deltachat-rpc-server-aarch64
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-aarch64"
path: "dist/deltachat-rpc-server-aarch64"
- name: Download deltachat-rpc-server-armv7
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-armv7"
path: "dist/deltachat-rpc-server-armv7"
- name: Download deltachat-rpc-server-win32.exe
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-win32.exe"
path: "dist/deltachat-rpc-server-win32.exe"
- name: Download deltachat-rpc-server-win64.exe
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-win64.exe"
path: "dist/deltachat-rpc-server-win64.exe"
- name: List downloaded artifacts
run: ls -l dist/
@@ -129,4 +123,4 @@ jobs:
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
dist/*
dist/deltachat-rpc-server-*

View File

@@ -1,176 +1,18 @@
# Changelog
## [1.115.0] - 2023-05-12
### JSON-RPC API Changes
- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)).
- Add API to get reactions outside the message snapshot.
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`.
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`.
- `JSONRPCReactions.reactions` is now a `Vec<JSONRPCReaction>` with unique reactions and their count, sorted in descending order.
- `Event`: `context_id` property is now called `contextId`.
- Expand `MessageSearchResult`:
- Always include `chat_name`(not an option anymore).
- Add `author_id`, `chat_type`, `chat_color`, `is_chat_protected`, `is_chat_contact_request`, `is_chat_archived`.
- `author_name` now contains the overridden sender name.
- `ChatListItemFetchResult` gets new properties: `summary_preview_image`, `last_message_type` and `last_message_id`
- New `MessageReadReceipt` type and `get_message_read_receipts(account_id, message_id)` jsonrpc method.
### API Changes
- New rust API `send_webxdc_status_update_struct` to send a `StatusUpdateItem`.
- Add `get_msg_read_receipts(context, msg_id)` - get the contacts that send read receipts for a message.
### Features / Changes
- Build deltachat-rpc-server releases for x86\_64 macOS.
- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)).
- Improve SMTP logging.
- Do not cut incoming text if "bot" config is set.
### Fixes
- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)).
- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)).
- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)).
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/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/deltachat/deltachat-core-rust/pull/4377)).
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)).
### Refactor
- Iterate over `msg_ids` without .iter().
## [1.112.9] - 2023-05-12
### Fixes
- Fetch at most 100 existing messages even if EXISTS was not received.
- Delete `smtp` rows when message sending is cancelled.
### Changes
- Improve SMTP logging.
## [1.114.0] - 2023-04-24
### Changes
- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events.
This better corresponds to JSON-RPC 2.0 server-client distinction
and is expected to simplify writing new bindings
because dispatching events can be done on higher level.
- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events.
### Fixes
- JSON-RPC: do not print to stdout on failure to find an account.
## [1.113.0] - 2023-04-18
### Added
- New JSON-RPC API `can_send()`.
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
These APIs can be used by bots to get all unprocessed messages
in the order of their arrival and wait for them without relying on events.
- New Python bindings API `Account.wait_next_incoming_message()`.
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
### Changes
- Increase MSRV to 1.65.0. #4236
- Remove upper limit on the attachment size. #4253
- Update rPGP to 0.10.1. #4236
- Compress HTML emails stored in the `mime_headers` column of the database.
- Strip BIDI characters in system messages, files, group names and contact names. #3479
- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`.
- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF.
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
in favor of `get_next_msgs()` and `wait_next_msgs()`.
- Remove metadata from avatars and JPEG images before sending. #4037
- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037
### Fixes
- Don't let blocking be bypassed using groups. #4316
- Show a warning if quota list is empty. #4261
- Do not reset status on other devices when sending signed reaction messages. #3692
- Update `accounts.toml` atomically.
- Fix python bindings README documentation on installing the bindings from source.
- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934
## [1.112.8] - 2023-04-20
### Changes
- Add `get_http_response` JSON-RPC API.
- Add C API to get HTTP responses.
## [1.112.7] - 2023-04-17
### Fixes
- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses.
## [1.112.6] - 2023-04-04
### Changes
- Add a device message after backup transfer #4301
### Fixed
- Updated `iroh` from 0.4.0 to 0.4.1 to fix transfer of large accounts with many blob files.
## [1.112.5] - 2023-04-02
### Fixes
- Run SQL database migrations after receiving a backup from the network. #4287
## [1.112.4] - 2023-03-31
### Fixes
- Fix call to `auditwheel` in `scripts/run_all.sh`.
## [1.112.3] - 2023-03-30
### Fixes
- `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 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
## [1.112.1] - 2023-03-27
### Changes
- Add support for `--version` argument to `deltachat-rpc-server`. #4224
It can be used to check the installed version without starting the server.
### Fixes
- deltachat-rpc-client: fix bug in `Chat.send_message()`: invalid `MessageData` field `quotedMsg` instead of `quotedMsgId`
- `receive_imf`: Mark special messages as seen. Exactly: delivery reports, webxdc status updates. #4230
## [1.112.0] - 2023-03-23
## [Unreleased]
### Changes
- "full message view" not needed because of footers that go to contact status #4151
- Pick up system's light/dark mode in generated message HTML #4150
- Support non-persistent configuration with DELTACHAT_* env
- Print deltachat-repl errors with causes. #4166
- Increase MSRV to 1.64. #4167
- Core takes care of stopping and re-starting IO itself where needed,
e.g. during backup creation.
It is no longer needed to call `dc_stop_io()`.
`dc_start_io()` can now be called at any time without harm. #4138
- Pick up system's light/dark mode in generated message HTML. #4150
- More accurate `maybe_add_bcc_self` device message text. #4175
- "Full message view" not needed because of footers that go to contact status. #4151
- Support non-persistent configuration with `DELTACHAT_*` env. #4154
- Print deltachat-repl errors with causes. #4166
e.g. during backup creation. It is no longer needed to call
dc_stop_io(). dc_start_io() can now be called at any time without
harm. #4138
- More accurate maybe_add_bcc_self device message text #4175
### Fixes
- Fix segmentation fault if `dc_context_unref()` is called during
@@ -181,11 +23,6 @@
- Do not emit "Failed to run incremental vacuum" warnings on success. #4160
- Ability to send backup over network and QR code to setup second device #4007
- Disable buffering during STARTTLS setup. #4190
- Add `DC_EVENT_IMAP_INBOX_IDLE` event to wait until the account
is ready for testing.
It is used to fix race condition between fetching
existing messages and starting the test. #4208
## [1.111.0] - 2023-03-05
@@ -316,6 +153,7 @@
- Do not treat invalid email addresses as an exception #3942
- Add timeouts to HTTP requests #3948
## 1.105.0
### Changes
@@ -401,6 +239,7 @@
- Disable read timeout during IMAP IDLE #3826
- Bots automatically accept mailing lists #3831
## 1.102.0
### Changes
@@ -2475,17 +2314,5 @@ For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...HEAD
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
[1.112.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.1...v1.112.2
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0

View File

@@ -1,70 +0,0 @@
# Contributing guidelines
## Reporting bugs
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
If the bug you found is specific to
[Android](https://github.com/deltachat/deltachat-android/issues),
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
report it to the corresponding repository.
## Proposing features
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
## Contributing code
If you want to contribute a code, [open a pull request](https://github.com/deltachat/deltachat-core-rust/pulls).
You can find the list of good first issues
and a link to this guide
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
### Coding conventions
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
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 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`"
- `test`: Test changes and improvements to the testing framework.
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
#### Breaking Changes
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
Alternatively, breaking changes can go into the commit description, e.g.:
```
fix: Fix race condition and db corruption when a message was received during backup
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
```
#### Multiple Changes in one PR
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
[Clippy]: https://doc.rust-lang.org/clippy/
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).

1357
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.115.0"
version = "1.111.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
rust-version = "1.64"
[profile.dev]
debug = 0
@@ -25,6 +25,7 @@ panic = 'abort'
opt-level = "z"
[patch.crates-io]
default-net = { git = "https://github.com/dignifiedquire/default-net.git", branch="feat-android" }
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
@@ -35,43 +36,43 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
async-imap = { version = "0.8.0", default-features = false, features = ["runtime-tokio"] }
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
backtrace = "0.3"
base64 = "0.21"
brotli = "3.3"
bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
futures = "0.3"
futures-lite = "1.13.0"
futures-lite = "1.12.0"
hex = "0.4.0"
humansize = "2"
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.1", default-features = false }
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
# iroh = { version = "0.3.0", default-features = false }
iroh = { git = 'https://github.com/n0-computer/iroh', branch = "flub/ticket-multiple-addrs" }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
mailparse = "0.14"
mime = "0.3.17"
num_cpus = "1.15"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.17.0"
percent-encoding = "2.2"
parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pgp = { version = "0.9", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.28"
quick-xml = "0.27"
rand = "0.8"
regex = "1.8"
reqwest = { version = "0.11.17", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
regex = "1.7"
reqwest = { version = "0.11.14", features = ["json"] }
rusqlite = { version = "0.28", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.4"
serde_json = "1.0"
@@ -84,11 +85,10 @@ strum_macros = "0.24"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.8"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.7"
trust-dns-resolver = "0.22"
url = "2"
@@ -97,12 +97,12 @@ uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.4.0", features = ["async_tokio"] }
futures-lite = "1.13"
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.7.3"
testdir = "0.7.2"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
[workspace]

View File

@@ -1,18 +0,0 @@
# Releasing a new version of DeltaChat core
For example, to release version 1.116.0 of the core, do the following steps.
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
3. Update the version by running `scripts/set_core_version.py 1.116.0`.
4. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
5. Tag the release: `git tag -a v1.116.0`.
6. Push the release tag: `git push origin v1.116.0`.
7. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -1,77 +0,0 @@
# configuration file for git-cliff
# see https://git-cliff.org/docs/configuration/
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = false
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features / Changes"},
{ message = "^fix", group = "Fixes"},
{ message = "^api", group = "API-Changes" },
{ message = "^refactor", group = "Refactor"},
{ message = "^perf", group = "Performance"},
{ message = "^test", group = "Tests"},
{ message = "^style", group = "Styling"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ message = "^build", group = "Build system"},
{ message = "^docs", group = "Documentation"},
{ message = "^ci", group = "CI"},
{ message = ".*", group = "Other"},
# { body = ".*security", group = "Security"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = true
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
#skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42
[changelog]
# changelog header
header = """
# Changelog\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}\
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
{{ commit.message | upper_first }}.\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.115.0"
version = "1.111.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -23,9 +23,8 @@ serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.8"
rand = "0.7"
once_cell = "1.17.0"
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
[features]
default = ["vendored"]

View File

@@ -25,7 +25,6 @@ typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
typedef struct _dc_http_response dc_http_response_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
@@ -73,6 +72,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
*
* The example above uses "pthreads",
* however, you can also use anything else for thread handling.
* All deltachat-core functions, unless stated otherwise, are thread-safe.
*
* Now you can **configure the context:**
*
@@ -142,72 +142,6 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
* ~~~
*
*
* ## Thread safety
*
* All deltachat-core functions, unless stated otherwise, are thread-safe.
* In particular, it is safe to pass the same dc_context_t pointer
* to multiple functions running concurrently in different threads.
*
* All the functions are guaranteed not to use the reference passed to them
* after returning. If the function spawns a long-running process,
* such as dc_configure() or dc_imex(), it will ensure that the objects
* passed to them are not deallocated as long as they are needed.
* For example, it is safe to call dc_imex(context, ...) and
* call dc_context_unref(context) immediately after return from dc_imex().
* It is however **not safe** to call dc_context_unref(context) concurrently
* until dc_imex() returns, because dc_imex() may have not increased
* the reference count of dc_context_t yet.
*
* This means that the context may be still in use after
* dc_context_unref() call.
* For example, it is possible to start the import/export process,
* call dc_context_unref(context) immediately after
* and observe #DC_EVENT_IMEX_PROGRESS events via the event emitter.
* Once dc_get_next_event() returns NULL,
* it is safe to terminate the application.
*
* It is recommended to create dc_context_t in the main thread
* and only call dc_context_unref() once other threads that may use it,
* such as the event loop thread, are terminated.
* Common mistake is to use dc_context_unref() as a way
* to cause dc_get_next_event() return NULL and terminate event loop this way.
* If event loop thread is inside a function taking dc_context_t
* as an argument at the moment dc_context_unref() is called on the main thread,
* the behavior is undefined.
*
* Recommended way to safely terminate event loop
* and shutdown the application is
* to use a boolean variable
* indicating that the event loop should stop
* and check it in the event loop thread
* every time before calling dc_get_next_event().
* To terminate the event loop, main thread should:
* 1. Notify background threads,
* such as event loop (blocking in dc_get_next_event())
* and message processing loop (blocking in dc_wait_next_msgs()),
* that they should terminate by atomically setting the
* boolean flag in the memory
* shared between the main thread and background loop threads.
* 2. Call dc_stop_io() or dc_accounts_stop_io(), depending
* on whether a single account or account manager is used.
* Stopping I/O is guaranteed to emit at least one event
* and interrupt the event loop even if it was blocked on dc_get_next_event().
* Stopping I/O is guaranteed to interrupt a single dc_wait_next_msgs().
* 3. Wait until the event loop thread notices the flag,
* exits the event loop and terminates.
* 4. Call dc_context_unref() or dc_accounts_unref().
* 5. Keep calling dc_get_next_event() in a loop until it returns NULL,
* indicating that the contexts are deallocated.
* 6. Terminate the application.
*
* When using C API via FFI in runtimes that use automatic memory management,
* such as CPython, JVM or Node.js, take care to ensure the correct
* shutdown order and avoid calling dc_context_unref() or dc_accounts_unref()
* on the objects still in use in other threads,
* e.g. by keeping a reference to the wrapper object.
* The details depend on the runtime being used.
*
*
* ## Class reference
*
* For a class reference, see the "Classes" link atop.
@@ -461,19 +395,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* 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,
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
* and does not cut large incoming text messages.
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
* The value is updated automatically
* when dc_markseen_msgs() is called,
* but the bot can also set it manually if it processed
* the message but does not want to mark it as seen.
* For most bots calling `dc_markseen_msgs()` is the
* recommended way to update this value
* even for self-sent messages.
* adds Auto-Submitted header to outgoing messages
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -1360,56 +1283,6 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
/**
* Returns the message IDs of all messages of any chat
* with a database ID higher than `last_msg_id` config value.
*
* This function is intended for use by bots.
* Self-sent messages, device messages,
* messages from contact requests
* and muted chats are included,
* but messages from explicitly blocked contacts
* and chats are ignored.
*
* This function may be called as a part of event loop
* triggered by DC_EVENT_INCOMING_MSG if you are only interested
* in the incoming messages.
* Otherwise use a separate message processing loop
* calling dc_wait_next_msgs() in a separate thread.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
* On errors, the list is empty. NULL is never returned.
*/
dc_array_t* dc_get_next_msgs (dc_context_t* context);
/**
* Waits for notification of new messages
* and returns an array of new message IDs.
* See the documentation for dc_get_next_msgs()
* for the details of return value.
*
* This function waits for internal notification of
* a new message in the database and returns afterwards.
* Notification is also sent when I/O is started
* to allow processing new messages
* and when I/O is stopped using dc_stop_io() or dc_accounts_stop_io()
* to allow for manual interruption of the message processing loop.
* The function may return an empty array if there are
* no messages after notification,
* which may happen on start or if the message is quickly deleted
* after adding it to the database.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
* On errors, the list is empty. NULL is never returned.
*/
dc_array_t* dc_wait_next_msgs (dc_context_t* context);
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
@@ -2009,11 +1882,6 @@ int dc_resend_msgs (dc_context_t* context, const uint3
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* This function updates last_msg_id configuration value
* to the maximum of the current value and IDs passed to this function.
* Bots which mark messages as seen can rely on this side effect
* to avoid updating last_msg_id value manually.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*
* @memberof dc_context_t
@@ -2847,12 +2715,6 @@ void dc_backup_provider_wait (dc_backup_provider_t* backup_provider);
/**
* Frees a dc_backup_provider_t object.
*
* If the provider has not yet finished, as indicated by
* dc_backup_provider_wait() or the #DC_EVENT_IMEX_PROGRESS event with value
* of 0 (failed) or 1000 (succeeded), this will also abort any in-progress
* transfer. If this aborts the provider a #DC_EVENT_IMEX_PROGRESS event with
* value 0 (failed) will be emitted.
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
@@ -5129,72 +4991,6 @@ int dc_provider_get_status (const dc_provider_t* prov
void dc_provider_unref (dc_provider_t* provider);
/**
* Return an HTTP(S) GET response.
* This function can be used to download remote content for HTML emails.
*
* @memberof dc_context_t
* @param context The context object to take proxy settings from.
* @param url HTTP or HTTPS URL.
* @return The response must be released using dc_http_response_unref() after usage.
* NULL is returned on errors.
*/
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
/**
* @class dc_http_response_t
*
* An object containing an HTTP(S) GET response.
* Created by dc_get_http_response().
*/
/**
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
/**
* Returns HTTP response encoding, e.g. "utf-8".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_encoding (const dc_http_response_t* response);
/**
* Returns HTTP response contents.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
*/
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
/**
* Returns HTTP response content size.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob size.
*/
size_t dc_http_response_get_size (const dc_http_response_t* response);
/**
* Free an HTTP response object.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
*/
void dc_http_response_unref (const dc_http_response_t* response);
/**
* @class dc_lot_t
*
@@ -5672,6 +5468,7 @@ void dc_reactions_unref (dc_reactions_t* reactions);
*/
/**
* @class dc_jsonrpc_instance_t
*
@@ -5720,18 +5517,6 @@ void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* req
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Make a JSON-RPC call and return a response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
*
@@ -5920,14 +5705,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
/**
* Emitted before going into IDLE on the Inbox folder.
*
* @param data1 0
* @param data2 0
*/
#define DC_EVENT_IMAP_INBOX_IDLE 106
/**
* Emitted when a new blob file was successfully written
*
@@ -7213,16 +6990,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
#define DC_STR_BACKUP_TRANSFER_QR 162
/// "Account transferred to your second device."
///
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/**
* @}
*/

View File

@@ -31,7 +31,6 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
@@ -161,8 +160,7 @@ pub unsafe extern "C" fn dc_context_open(
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.open(passphrase))
.context("dc_context_open() failed")
.log_err(ctx)
.log_err(ctx, "dc_context_open() failed")
.map(|b| b as libc::c_int)
.unwrap_or(0)
}
@@ -218,18 +216,16 @@ pub unsafe extern "C" fn dc_set_config(
if key.starts_with("ui.") {
ctx.set_ui_config(&key, value.as_deref())
.await
.with_context(|| format!("dc_set_config failed: Can't set {key} to {value:?}"))
.log_err(ctx)
.with_context(|| format!("Can't set {key} to {value:?}"))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int
} else {
match config::Config::from_str(&key) {
Ok(key) => ctx
.set_config(key, value.as_deref())
.await
.with_context(|| {
format!("dc_set_config() failed: Can't set {key} to {value:?}")
})
.log_err(ctx)
.with_context(|| format!("Can't set {key} to {value:?}"))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int,
Err(_) => {
warn!(ctx, "dc_set_config(): invalid key");
@@ -257,8 +253,7 @@ pub unsafe extern "C" fn dc_get_config(
if key.starts_with("ui.") {
ctx.get_ui_config(&key)
.await
.context("Can't get ui-config")
.log_err(ctx)
.log_err(ctx, "Can't get ui-config")
.unwrap_or_default()
.unwrap_or_default()
.strdup()
@@ -267,8 +262,7 @@ pub unsafe extern "C" fn dc_get_config(
Ok(key) => ctx
.get_config(key)
.await
.context("Can't get config")
.log_err(ctx)
.log_err(ctx, "Can't get config")
.unwrap_or_default()
.unwrap_or_default()
.strdup(),
@@ -420,8 +414,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
block_on(async move {
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
.await
.context("dc_get_oauth2_url failed")
.log_err(ctx)
.log_err(ctx, "dc_get_oauth2_url failed")
{
Ok(Some(res)) => res.strdup(),
Ok(None) | Err(_) => ptr::null_mut(),
@@ -430,12 +423,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
}
fn spawn_configure(ctx: Context) {
spawn(async move {
ctx.configure()
.await
.context("Configure failed")
.log_err(&ctx)
});
spawn(async move { ctx.configure().await.log_err(&ctx, "Configure failed") });
}
#[no_mangle]
@@ -460,8 +448,7 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
block_on(async move {
ctx.is_configured()
.await
.context("failed to get configured state")
.log_err(ctx)
.log_err(ctx, "failed to get configured state")
.unwrap_or_default() as libc::c_int
})
}
@@ -513,7 +500,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::SmtpMessageSent(_) => 103,
EventType::ImapMessageDeleted(_) => 104,
EventType::ImapMessageMoved(_) => 105,
EventType::ImapInboxIdle => 106,
EventType::NewBlobFile(_) => 150,
EventType::DeletedBlobFile(_) => 151,
EventType::Warning(_) => 300,
@@ -558,7 +544,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SmtpMessageSent(_)
| EventType::ImapMessageDeleted(_)
| EventType::ImapMessageMoved(_)
| EventType::ImapInboxIdle
| EventType::NewBlobFile(_)
| EventType::DeletedBlobFile(_)
| EventType::Warning(_)
@@ -609,7 +594,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SmtpMessageSent(_)
| EventType::ImapMessageDeleted(_)
| EventType::ImapMessageMoved(_)
| EventType::ImapInboxIdle
| EventType::NewBlobFile(_)
| EventType::DeletedBlobFile(_)
| EventType::Warning(_)
@@ -669,7 +653,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::ImapInboxIdle
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
| EventType::MsgFailed { .. }
@@ -803,8 +786,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
Ok::<_, anyhow::Error>(1)
})
.context("Failed to save keypair")
.log_err(ctx)
.log_err(ctx, "Failed to save keypair")
.unwrap_or(0)
}
@@ -831,8 +813,7 @@ pub unsafe extern "C" fn dc_get_chatlist(
block_on(async move {
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
.await
.context("Failed to get chatlist")
.log_err(ctx)
.log_err(ctx, "Failed to get chatlist")
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
@@ -857,8 +838,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
block_on(async move {
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
.await
.context("Failed to create chat from contact_id")
.log_err(ctx)
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -878,8 +858,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
block_on(async move {
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
.await
.context("Failed to get chat for contact_id")
.log_err(ctx)
.log_err(ctx, "Failed to get chat for contact_id")
.unwrap_or_default() // unwraps the Result
.map(|id| id.to_u32())
.unwrap_or(0) // unwraps the Option
@@ -1021,8 +1000,7 @@ pub unsafe extern "C" fn dc_get_msg_reactions(
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.context("failed dc_get_msg_reactions() call")
.log_err(ctx)
.log_err(ctx, "failed dc_get_msg_reactions() call")
{
reactions
} else {
@@ -1050,8 +1028,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
&to_string_lossy(json),
&to_string_lossy(descr),
))
.context("Failed to send webxdc update")
.log_err(ctx)
.log_err(ctx, "Failed to send webxdc update")
.is_ok() as libc::c_int
}
@@ -1270,8 +1247,7 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
let arr = dc_array_t::from(
ctx.get_fresh_msgs()
.await
.context("Failed to get fresh messages")
.log_err(ctx)
.log_err(ctx, "Failed to get fresh messages")
.unwrap_or_default()
.iter()
.map(|msg_id| msg_id.to_u32())
@@ -1281,50 +1257,6 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_next_msgs(context: *mut dc_context_t) -> *mut dc_array::dc_array_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_next_msgs()");
return ptr::null_mut();
}
let ctx = &*context;
let msg_ids = block_on(ctx.get_next_msgs())
.context("failed to get next messages")
.log_err(ctx)
.unwrap_or_default();
let arr = dc_array_t::from(
msg_ids
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
}
#[no_mangle]
pub unsafe extern "C" fn dc_wait_next_msgs(
context: *mut dc_context_t,
) -> *mut dc_array::dc_array_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_wait_next_msgs()");
return ptr::null_mut();
}
let ctx = &*context;
let msg_ids = block_on(ctx.wait_next_msgs())
.context("failed to wait for next messages")
.log_err(ctx)
.unwrap_or_default();
let arr = dc_array_t::from(
msg_ids
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
}
#[no_mangle]
pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id: u32) {
if context.is_null() {
@@ -1336,8 +1268,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
block_on(async move {
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
.await
.context("Failed marknoticed chat")
.log_err(ctx)
.log_err(ctx, "Failed marknoticed chat")
.unwrap_or(())
})
}
@@ -1479,8 +1410,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
ChatId::new(chat_id)
.set_visibility(ctx, visibility)
.await
.context("Failed setting chat visibility")
.log_err(ctx)
.log_err(ctx, "Failed setting chat visibility")
.unwrap_or(())
})
}
@@ -1497,9 +1427,7 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
ChatId::new(chat_id)
.delete(ctx)
.await
.context("Failed chat delete")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Failed chat delete");
})
}
@@ -1515,9 +1443,7 @@ pub unsafe extern "C" fn dc_block_chat(context: *mut dc_context_t, chat_id: u32)
ChatId::new(chat_id)
.block(ctx)
.await
.context("Failed chat block")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Failed chat block");
})
}
@@ -1533,9 +1459,7 @@ pub unsafe extern "C" fn dc_accept_chat(context: *mut dc_context_t, chat_id: u32
ChatId::new(chat_id)
.accept(ctx)
.await
.context("Failed chat accept")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Failed chat accept");
})
}
@@ -1633,8 +1557,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
block_on(async move {
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
.await
.context("Failed to create group chat")
.log_err(ctx)
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -1648,8 +1571,7 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
}
let ctx = &*context;
block_on(chat::create_broadcast_list(ctx))
.context("Failed to create broadcast list")
.log_err(ctx)
.log_err(ctx, "Failed to create broadcast list")
.map(|id| id.to_u32())
.unwrap_or(0)
}
@@ -1671,8 +1593,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
ChatId::new(chat_id),
ContactId::new(contact_id),
))
.context("is_contact_in_chat failed")
.log_err(ctx)
.log_err(ctx, "is_contact_in_chat failed")
.unwrap_or_default() as libc::c_int
}
@@ -1693,8 +1614,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
ChatId::new(chat_id),
ContactId::new(contact_id),
))
.context("Failed to add contact")
.log_err(ctx)
.log_err(ctx, "Failed to add contact")
.is_ok() as libc::c_int
}
@@ -1715,8 +1635,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
ChatId::new(chat_id),
ContactId::new(contact_id),
))
.context("Failed to remove contact")
.log_err(ctx)
.log_err(ctx, "Failed to remove contact")
.is_ok() as libc::c_int
}
@@ -1835,8 +1754,7 @@ pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
// ignored when ephemeral timer value is used to construct
// message headers.
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
.context("Failed to get ephemeral timer")
.log_err(ctx)
.log_err(ctx, "Failed to get ephemeral timer")
.unwrap_or_default()
.to_u32()
}
@@ -1857,8 +1775,7 @@ pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
ChatId::new(chat_id)
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
.await
.context("Failed to set ephemeral timer")
.log_err(ctx)
.log_err(ctx, "Failed to set ephemeral timer")
.is_ok() as libc::c_int
})
}
@@ -1934,8 +1851,7 @@ pub unsafe extern "C" fn dc_delete_msgs(
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
block_on(message::delete_msgs(ctx, &msg_ids))
.context("failed dc_delete_msgs() call")
.log_err(ctx)
.log_err(ctx, "failed dc_delete_msgs() call")
.ok();
}
@@ -1999,8 +1915,7 @@ pub unsafe extern "C" fn dc_markseen_msgs(
let ctx = &*context;
block_on(message::markseen_msgs(ctx, msg_ids))
.context("failed dc_markseen_msgs() call")
.log_err(ctx)
.log_err(ctx, "failed dc_markseen_msgs() call")
.ok();
}
@@ -2042,8 +1957,7 @@ pub unsafe extern "C" fn dc_download_full_msg(context: *mut dc_context_t, msg_id
}
let ctx = &*context;
block_on(MsgId::new(msg_id).download_full(ctx))
.context("Failed to download message fully.")
.log_err(ctx)
.log_err(ctx, "Failed to download message fully.")
.ok();
}
@@ -2091,8 +2005,7 @@ pub unsafe extern "C" fn dc_create_contact(
let name = to_string_lossy(name);
block_on(Contact::create(ctx, &name, &to_string_lossy(addr)))
.context("Cannot create contact")
.log_err(ctx)
.log_err(ctx, "Cannot create contact")
.map(|id| id.to_u32())
.unwrap_or(0)
}
@@ -2169,8 +2082,7 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
Box::into_raw(Box::new(dc_array_t::from(
Contact::get_all_blocked(ctx)
.await
.context("Can't get blocked contacts")
.log_err(ctx)
.log_err(ctx, "Can't get blocked contacts")
.unwrap_or_default()
.iter()
.map(|id| id.to_u32())
@@ -2195,15 +2107,11 @@ pub unsafe extern "C" fn dc_block_contact(
if block == 0 {
Contact::unblock(ctx, contact_id)
.await
.context("Can't unblock contact")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Can't unblock contact");
} else {
Contact::block(ctx, contact_id)
.await
.context("Can't block contact")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Can't block contact");
}
});
}
@@ -2276,8 +2184,7 @@ fn spawn_imex(ctx: Context, what: imex::ImexMode, param1: String, passphrase: Op
spawn(async move {
imex::imex(&ctx, what, param1.as_ref(), passphrase)
.await
.context("IMEX failed")
.log_err(&ctx)
.log_err(&ctx, "IMEX failed")
});
}
@@ -2463,8 +2370,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
.await
.map(|chatid| chatid.to_u32())
.context("failed dc_join_securejoin() call")
.log_err(ctx)
.log_err(ctx, "failed dc_join_securejoin() call")
.unwrap_or_default()
})
}
@@ -2486,8 +2392,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
ChatId::new(chat_id),
seconds as i64,
))
.context("Failed dc_send_locations_to_chat()")
.log_err(ctx)
.log_err(ctx, "Failed dc_send_locations_to_chat()")
.ok();
}
@@ -2570,8 +2475,7 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
block_on(async move {
location::delete_all(ctx)
.await
.context("Failed to delete locations")
.log_err(ctx)
.log_err(ctx, "Failed to delete locations")
.ok()
});
}
@@ -2855,8 +2759,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
.list
.get_summary(ctx, index, maybe_chat)
.await
.context("get_summary failed")
.log_err(ctx)
.log_err(ctx, "get_summary failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
})
@@ -2884,8 +2787,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
msg_id,
None,
))
.context("get_summary2 failed")
.log_err(ctx)
.log_err(ctx, "get_summary2 failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
@@ -3068,8 +2970,7 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
let ffi_chat = &*chat;
let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.can_send(ctx))
.context("can_send failed")
.log_err(ctx)
.log_err(ctx, "can_send failed")
.unwrap_or_default() as libc::c_int
}
@@ -3514,8 +3415,7 @@ pub unsafe extern "C" fn dc_msg_get_summary(
let ctx = &*ffi_msg.context;
let summary = block_on(ffi_msg.message.get_summary(ctx, maybe_chat))
.context("dc_msg_get_summary failed")
.log_err(ctx)
.log_err(ctx, "dc_msg_get_summary failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
@@ -3533,8 +3433,7 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
let ctx = &*ffi_msg.context;
let summary = block_on(ffi_msg.message.get_summary(ctx, None))
.context("dc_msg_get_summarytext failed")
.log_err(ctx)
.log_err(ctx, "dc_msg_get_summarytext failed")
.unwrap_or_default();
match usize::try_from(approx_characters) {
Ok(chars) => summary.truncated_text(chars).strdup(),
@@ -3801,9 +3700,7 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
.message
.latefiling_mediasize(ctx, width, height, duration)
})
.context("Cannot set media size")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Cannot set media size");
}
#[no_mangle]
@@ -3842,8 +3739,7 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
.message
.set_quote(&*ffi_msg.context, quote_msg)
.await
.context("failed to set quote")
.log_err(&*ffi_msg.context)
.log_err(&*ffi_msg.context, "failed to set quote")
.ok();
});
}
@@ -3874,8 +3770,7 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
.message
.quoted_message(context)
.await
.context("failed to get quoted message")
.log_err(context)
.log_err(context, "failed to get quoted message")
.unwrap_or(None)
});
@@ -3898,8 +3793,7 @@ pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_
.message
.parent(context)
.await
.context("failed to get parent message")
.log_err(context)
.log_err(context, "failed to get parent message")
.unwrap_or(None)
});
@@ -4090,8 +3984,7 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.is_verified(ctx))
.context("is_verified failed")
.log_err(ctx)
.log_err(ctx, "is_verified failed")
.unwrap_or_default() as libc::c_int
}
@@ -4106,8 +3999,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr(
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.get_verifier_addr(ctx))
.context("failed to get verifier for contact")
.log_err(ctx)
.log_err(ctx, "failed to get verifier for contact")
.unwrap_or_default()
.strdup()
}
@@ -4121,8 +4013,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx))
.context("failed to get verifier")
.log_err(ctx)
.log_err(ctx, "failed to get verifier")
.unwrap_or_default()
.unwrap_or_default();
@@ -4274,8 +4165,8 @@ pub unsafe extern "C" fn dc_backup_provider_new(
provider,
})
.map(|ffi_provider| Box::into_raw(Box::new(ffi_provider)))
.log_err(ctx, "BackupProvider failed")
.context("BackupProvider failed")
.log_err(ctx)
.set_last_error(ctx)
.unwrap_or(ptr::null_mut())
}
@@ -4291,8 +4182,8 @@ pub unsafe extern "C" fn dc_backup_provider_get_qr(
let ffi_provider = &*provider;
let ctx = &*ffi_provider.context;
deltachat::qr::format_backup(&ffi_provider.provider.qr())
.log_err(ctx, "BackupProvider get_qr failed")
.context("BackupProvider get_qr failed")
.log_err(ctx)
.set_last_error(ctx)
.unwrap_or_default()
.strdup()
@@ -4310,8 +4201,8 @@ pub unsafe extern "C" fn dc_backup_provider_get_qr_svg(
let ctx = &*ffi_provider.context;
let provider = &ffi_provider.provider;
block_on(generate_backup_qr(ctx, &provider.qr()))
.log_err(ctx, "BackupProvider get_qr_svg failed")
.context("BackupProvider get_qr_svg failed")
.log_err(ctx)
.set_last_error(ctx)
.unwrap_or_default()
.strdup()
@@ -4327,18 +4218,14 @@ pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provid
let ctx = &*ffi_provider.context;
let provider = &mut ffi_provider.provider;
block_on(provider)
.log_err(ctx, "Failed to await BackupProvider")
.context("Failed to await BackupProvider")
.log_err(ctx)
.set_last_error(ctx)
.ok();
}
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_unref(provider: *mut dc_backup_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_backup_provider_unref()");
return;
}
drop(Box::from_raw(provider));
}
@@ -4353,28 +4240,17 @@ pub unsafe extern "C" fn dc_receive_backup(
}
let ctx = &*context;
let qr_text = to_string_lossy(qr);
receive_backup(ctx.clone(), qr_text)
}
// Because this is a long-running operation make sure we own the Context. This stops a FFI
// user from deallocating it by calling unref on the object while we are using it.
fn receive_backup(ctx: Context, qr_text: String) -> libc::c_int {
let qr = match block_on(qr::check_qr(&ctx, &qr_text))
.context("Invalid QR code")
.log_err(&ctx)
.set_last_error(&ctx)
{
let qr = match block_on(qr::check_qr(ctx, &qr_text)).log_err(ctx, "Invalid QR code") {
Ok(qr) => qr,
Err(_) => return 0,
};
match block_on(imex::get_backup(&ctx, qr))
.context("Get backup failed")
.log_err(&ctx)
.set_last_error(&ctx)
{
Ok(_) => 1,
Err(_) => 0,
}
spawn(async move {
imex::get_backup(ctx, qr)
.await
.log_err(ctx, "Get backup failed")
.ok();
});
1
}
trait ResultExt<T, E> {
@@ -4421,8 +4297,8 @@ where
///
/// ```no_compile
/// some_dc_rust_api_call_returning_result()
/// .log_err(&context, "My API call failed")
/// .context("My API call failed")
/// .log_err(&context)
/// .set_last_error(&context)
/// .unwrap_or_default()
/// ```
@@ -4509,8 +4385,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.context("Can't get config")
.log_err(ctx)
.log_err(ctx, "Can't get config")
});
match socks5_enabled {
@@ -4573,96 +4448,6 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
// this may change once we start localizing string.
}
// dc_http_response_t
pub type dc_http_response_t = net::HttpResponse;
#[no_mangle]
pub unsafe extern "C" fn dc_get_http_response(
context: *const dc_context_t,
url: *const libc::c_char,
) -> *mut dc_http_response_t {
if context.is_null() || url.is_null() {
eprintln!("ignoring careless call to dc_get_http_response()");
return ptr::null_mut();
}
let context = &*context;
let url = to_string_lossy(url);
if let Ok(response) = block_on(read_url_blob(context, &url))
.context("read_url_blob")
.log_err(context)
{
Box::into_raw(Box::new(response))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_mimetype(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
return ptr::null_mut();
}
let response = &*response;
response.mimetype.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_encoding(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_encoding()");
return ptr::null_mut();
}
let response = &*response;
response.encoding.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_blob(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_blob()");
return ptr::null_mut();
}
let response = &*response;
let blob_len = response.blob.len();
let ptr = libc::malloc(blob_len);
libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
ptr as *mut libc::c_char
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_size(
response: *const dc_http_response_t,
) -> libc::size_t {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_size()");
return 0;
}
let response = &*response;
response.blob.len()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_unref()");
return;
}
drop(Box::from_raw(response));
}
// -- Accounts
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
@@ -4967,13 +4752,15 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use super::*;
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
event_thread: JoinHandle<Result<(), anyhow::Error>>,
}
#[no_mangle]
@@ -4986,12 +4773,28 @@ mod jsonrpc {
}
let account_manager = &*account_manager;
let events = block_on(account_manager.read()).get_event_emitter();
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let handle = RpcSession::new(request_handle.clone(), cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
let event_thread = spawn(async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
request_handle
.send_notification("event", Some(event))
.await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
});
let instance = dc_jsonrpc_instance_t {
receiver,
handle,
event_thread,
};
Box::into_raw(Box::new(instance))
}
@@ -5002,6 +4805,7 @@ mod jsonrpc {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
(*jsonrpc_instance).event_thread.abort();
drop(Box::from_raw(jsonrpc_instance));
}
@@ -5039,29 +4843,4 @@ mod jsonrpc {
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
method: *const libc::c_char,
params: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
match res {
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.115.0"
version = "1.111.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -19,21 +19,21 @@ serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.96"
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
futures = { version = "0.3.26" }
serde_json = "1.0.91"
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.28.0" }
tokio = { version = "1.25.0" }
sanitize-filename = "0.4"
walkdir = "2.3.3"
walkdir = "2.3.2"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.18", optional = true, features = ["ws"] }
axum = { version = "0.6.11", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.28.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -1,29 +1,19 @@
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
use deltachat::{Event, EventType};
use serde::Serialize;
use serde_json::{json, Value};
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct Event {
/// Event payload.
event: EventType,
/// Account ID.
context_id: u32,
}
impl From<CoreEvent> for Event {
fn from(event: CoreEvent) -> Self {
Event {
event: event.typ.into(),
context_id: event.id,
}
}
pub fn event_to_json_rpc_notification(event: Event) -> Value {
let id: JSONRPCEventType = event.typ.into();
json!({
"event": id,
"contextId": event.id,
})
}
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum EventType {
#[serde(tag = "type", rename = "Event")]
pub enum JSONRPCEventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
@@ -57,9 +47,6 @@ pub enum EventType {
msg: String,
},
/// Emitted before going into IDLE on the Inbox folder.
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile {
file: String,
@@ -296,27 +283,26 @@ pub enum EventType {
},
}
impl From<CoreEventType> for EventType {
fn from(event: CoreEventType) -> Self {
use EventType::*;
impl From<EventType> for JSONRPCEventType {
fn from(event: EventType) -> Self {
use JSONRPCEventType::*;
match event {
CoreEventType::Info(msg) => Info { msg },
CoreEventType::SmtpConnected(msg) => SmtpConnected { msg },
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
CoreEventType::ImapInboxIdle => ImapInboxIdle,
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
CoreEventType::Warning(msg) => Warning { msg },
CoreEventType::Error(msg) => Error { msg },
CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
EventType::Info(msg) => Info { msg },
EventType::SmtpConnected(msg) => SmtpConnected { msg },
EventType::ImapConnected(msg) => ImapConnected { msg },
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
EventType::NewBlobFile(file) => NewBlobFile { file },
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
EventType::Warning(msg) => Warning { msg },
EventType::Error(msg) => Error { msg },
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::ReactionsChanged {
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
@@ -325,76 +311,92 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed {
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::MsgRead { chat_id, msg_id } => MsgRead {
EventType::MsgRead { chat_id, msg_id } => MsgRead {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::ChatModified(chat_id) => ChatModified {
EventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
},
CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => {
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
ChatEphemeralTimerModified {
chat_id: chat_id.to_u32(),
timer: timer.to_u32(),
}
}
CoreEventType::ContactsChanged(contact) => ContactsChanged {
EventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()),
},
CoreEventType::LocationChanged(contact) => LocationChanged {
EventType::LocationChanged(contact) => LocationChanged {
contact_id: contact.map(|c| c.to_u32()),
},
CoreEventType::ConfigureProgress { progress, comment } => {
EventType::ConfigureProgress { progress, comment } => {
ConfigureProgress { progress, comment }
}
CoreEventType::ImexProgress(progress) => ImexProgress { progress },
CoreEventType::ImexFileWritten(path) => ImexFileWritten {
EventType::ImexProgress(progress) => ImexProgress { progress },
EventType::ImexFileWritten(path) => ImexFileWritten {
path: path.to_str().unwrap_or_default().to_owned(),
},
CoreEventType::SecurejoinInviterProgress {
EventType::SecurejoinInviterProgress {
contact_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
progress,
},
CoreEventType::SecurejoinJoinerProgress {
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => SecurejoinJoinerProgress {
contact_id: contact_id.to_u32(),
progress,
},
CoreEventType::ConnectivityChanged => ConnectivityChanged,
CoreEventType::SelfavatarChanged => SelfavatarChanged,
CoreEventType::WebxdcStatusUpdate {
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => WebxdcStatusUpdate {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
}
}
}
#[cfg(test)]
#[test]
fn generate_events_ts_types_definition() {
let events = {
let mut buf = Vec::new();
let options = typescript_type_def::DefinitionFileOptions {
root_namespace: None,
..typescript_type_def::DefinitionFileOptions::default()
};
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
.unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();
}

View File

@@ -4,7 +4,6 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::message::get_msg_read_receipts;
use deltachat::qr::Qr;
use deltachat::{
chat::{
@@ -25,7 +24,7 @@ use deltachat::{
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
reaction::{get_msg_reactions, send_reaction},
reaction::send_reaction,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
@@ -36,17 +35,17 @@ use tokio::sync::{watch, Mutex, RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
pub mod events;
pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::message::MessageData;
use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
@@ -165,16 +164,6 @@ impl CommandApi {
get_info()
}
/// Get the next event.
async fn get_next_event(&self) -> Result<Event> {
let event_emitter = self.accounts.read().await.get_event_emitter();
event_emitter
.recv()
.await
.map(|event| event.into())
.context("event channel is closed")
}
// ---------------------------------------------
// Account Management
// ---------------------------------------------
@@ -216,6 +205,8 @@ impl CommandApi {
let context_option = self.accounts.read().await.get_account(id);
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
println!("account with id {id} doesn't exist anymore");
}
}
Ok(accounts)
@@ -462,49 +453,6 @@ impl CommandApi {
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
/// Gets messages to be processed by the bot and returns their IDs.
///
/// Only messages with database ID higher than `last_msg_id` config value
/// are returned. After processing the messages, the bot should
/// update `last_msg_id` by calling [`markseen_msgs`]
/// or manually updating the value to avoid getting already
/// processed messages.
///
/// [`markseen_msgs`]: Self::markseen_msgs
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids = ctx
.get_next_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect();
Ok(msg_ids)
}
/// Waits for messages to be processed by the bot and returns their IDs.
///
/// This function is similar to [`get_next_msgs`],
/// but waits for internal new message notification before returning.
/// New message notification is sent when new message is added to the database,
/// on initialization, when I/O is started and when I/O is stopped.
/// This allows bots to use `wait_next_msgs` in a loop to process
/// old messages after initialization and during the bot runtime.
/// To shutdown the bot, stopping I/O can be used to interrupt
/// pending or next `wait_next_msgs` call.
///
/// [`get_next_msgs`]: Self::get_next_msgs
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids = ctx
.wait_next_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect();
Ok(msg_ids)
}
/// Estimate the number of messages that will be deleted
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
/// This is typically used to show the estimated impact to the user
@@ -548,7 +496,7 @@ impl CommandApi {
list_flags: Option<u32>,
query_string: Option<String>,
query_contact_id: Option<u32>,
) -> Result<Vec<u32>> {
) -> Result<Vec<ChatListEntry>> {
let ctx = self.get_context(account_id).await?;
let list = Chatlist::try_load(
&ctx,
@@ -557,9 +505,12 @@ impl CommandApi {
query_contact_id.map(ContactId::new),
)
.await?;
let mut l: Vec<u32> = Vec::with_capacity(list.len());
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(list.get_chat_id(i)?.to_u32());
l.push(ChatListEntry(
list.get_chat_id(i)?.to_u32(),
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
));
}
Ok(l)
}
@@ -567,18 +518,19 @@ impl CommandApi {
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
entries: Vec<u32>,
entries: Vec<ChatListEntry>,
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
// todo custom json deserializer for ChatListEntry?
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<u32, ChatListItemFetchResult> =
HashMap::with_capacity(entries.len());
for &entry in entries.iter() {
for entry in entries.iter() {
result.insert(
entry,
entry.0,
match get_chat_list_item_by_id(&ctx, entry).await {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry,
id: entry.0,
error: format!("{err:#}"),
},
},
@@ -992,11 +944,6 @@ impl CommandApi {
/// Moreover, timer is started for incoming ephemeral messages.
/// This also happens for contact requests chats.
///
/// This function updates `last_msg_id` configuration value
/// to the maximum of the current value and IDs passed to this function.
/// Bots which mark messages as seen can rely on this side effect
/// to avoid updating `last_msg_id` value manually.
///
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1118,24 +1065,6 @@ impl CommandApi {
get_msg_info(&ctx, MsgId::new(message_id)).await
}
/// Returns contacts that sent read receipts and the time of reading.
async fn get_message_read_receipts(
&self,
account_id: u32,
message_id: u32,
) -> Result<Vec<MessageReadReceipt>> {
let ctx = self.get_context(account_id).await?;
let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
.await?
.iter()
.map(|(contact_id, ts)| MessageReadReceipt {
contact_id: contact_id.to_u32(),
timestamp: *ts,
})
.collect();
Ok(receipts)
}
/// Asks the core to start downloading a message fully.
/// This function is typically called when the user hits the "Download" button
/// that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
@@ -1153,17 +1082,17 @@ impl CommandApi {
}
/// Search messages containing the given query string.
/// Searching can be done globally (chat_id=None) or in a specified chat only (chat_id set).
/// Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
///
/// Global search results are typically displayed using dc_msg_get_summary(), chat
/// search results may just highlight the corresponding messages and present a
/// Global chat results are typically displayed using dc_msg_get_summary(), chat
/// search results may just hilite the corresponding messages and present a
/// prev/next button.
///
/// For the global search, the result is limited to 1000 messages,
/// this allows an incremental search done fast.
/// So, when getting exactly 1000 messages, the result actually may be truncated;
/// the UIs may display sth. like "1000+ messages found" in this case.
/// The chat search (if chat_id is set) is not limited.
/// For global search, result is limited to 1000 messages,
/// this allows incremental search done fast.
/// So, when getting exactly 1000 results, the result may be truncated;
/// the UIs may display sth. as "1000+ messages found" in this case.
/// Chat search (if a chat_id is set) is not limited.
async fn search_messages(
&self,
account_id: u32,
@@ -1681,15 +1610,6 @@ impl CommandApi {
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
async fn get_http_response(&self, account_id: u32, url: String) -> Result<HttpResponse> {
let ctx = self.get_context(account_id).await?;
let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
Ok(response)
}
/// Forward messages to another chat.
///
/// All types of messages can be forwarded,
@@ -1739,21 +1659,6 @@ impl CommandApi {
Ok(message_id.to_u32())
}
/// Returns reactions to the message.
async fn get_message_reactions(
&self,
account_id: u32,
message_id: u32,
) -> Result<Option<JSONRPCReactions>> {
let ctx = self.get_context(account_id).await?;
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
if reactions.is_empty() {
Ok(None)
} else {
Ok(Some(reactions.into()))
}
}
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
@@ -1796,15 +1701,6 @@ impl CommandApi {
Ok(msg_id)
}
/// Checks if messages can be sent to a given chat.
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(&ctx, chat_id).await?;
let can_send = chat.can_send(&ctx).await?;
Ok(can_send)
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context as _, Result};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
@@ -53,9 +53,7 @@ impl FullChat {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::load_from_db(context, *contact_id)
.await
.context("failed to load contact")?,
Contact::load_from_db(context, *contact_id).await?,
)
.await?,
)
@@ -75,8 +73,7 @@ impl FullChat {
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::load_from_db(context, *contact)
.await
.context("failed to load contact for was_seen_recently")?
.await?
.was_seen_recently(),
None => false,
}
@@ -92,7 +89,10 @@ impl FullChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
@@ -155,7 +155,10 @@ impl BasicChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,

View File

@@ -1,18 +1,22 @@
use anyhow::{Context, Result};
use deltachat::chat::{Chat, ChatId};
use deltachat::chatlist::get_last_message_for_chat;
use anyhow::Result;
use deltachat::constants::*;
use deltachat::contact::{Contact, ContactId};
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
};
use deltachat::{
chat::{Chat, ChatId},
message::MsgId,
};
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Deserialize, Serialize, TypeDef)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
@@ -27,8 +31,6 @@ pub enum ChatListItemFetchResult {
summary_text1: String,
summary_text2: String,
summary_status: u32,
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
is_group: bool,
fresh_message_counter: usize,
@@ -45,8 +47,6 @@ pub enum ChatListItemFetchResult {
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
last_message_type: Option<MessageViewtype>,
last_message_id: Option<u32>,
},
#[serde(rename_all = "camelCase")]
ArchiveLink { fresh_message_counter: usize },
@@ -56,9 +56,14 @@ pub enum ChatListItemFetchResult {
pub(crate) async fn get_chat_list_item_by_id(
ctx: &deltachat::context::Context,
entry: u32,
entry: &ChatListEntry,
) -> Result<ChatListItemFetchResult> {
let chat_id = ChatId::new(entry);
let chat_id = ChatId::new(entry.0);
let last_msgid = match entry.1 {
0 => None,
_ => Some(MsgId::new(entry.1)),
};
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
if chat_id.is_archived_link() {
@@ -67,18 +72,12 @@ pub(crate) async fn get_chat_list_item_by_id(
});
}
let last_msgid = get_last_message_for_chat(ctx, chat_id).await?;
let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat))
.await
.context("summary")?;
let chat = Chat::load_from_db(ctx, chat_id).await?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
let summary_preview_image = summary.thumbnail_path;
let visibility = chat.get_visibility();
let avatar_path = chat
@@ -86,15 +85,12 @@ pub(crate) async fn get_chat_list_item_by_id(
.await?
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
let (last_updated, message_type) = match last_msgid {
let last_updated = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
Some(last_message.get_timestamp() * 1000)
}
None => (None, None),
None => None,
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
@@ -105,8 +101,7 @@ pub(crate) async fn get_chat_list_item_by_id(
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
Some(contact) => Contact::load_from_db(ctx, *contact)
.await
.context("contact")?
.await?
.was_seen_recently(),
None => false,
};
@@ -129,7 +124,6 @@ pub(crate) async fn get_chat_list_item_by_id(
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
@@ -144,7 +138,5 @@ pub(crate) async fn get_chat_list_item_by_id(
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
last_message_type: message_type,
last_message_id: last_msgid.map(|id| id.to_u32()),
})
}

View File

@@ -1,29 +0,0 @@
use deltachat::net::HttpResponse as CoreHttpResponse;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
pub struct HttpResponse {
/// base64-encoded response body.
blob: String,
/// MIME type, e.g. "text/plain" or "text/html".
mimetype: Option<String>,
/// Encoding, e.g. "utf-8".
encoding: Option<String>,
}
impl From<CoreHttpResponse> for HttpResponse {
fn from(response: CoreHttpResponse) -> Self {
use base64::{engine::general_purpose, Engine as _};
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
let mimetype = response.mimetype;
let encoding = response.encoding;
HttpResponse {
blob,
mimetype,
encoding,
}
}
}

View File

@@ -1,7 +1,7 @@
use anyhow::{Context as _, Result};
use anyhow::{anyhow, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
use deltachat::chat::ChatVisibility;
use deltachat::constants::Chattype;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
@@ -114,12 +114,8 @@ impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::load_from_db(context, message.get_from_id())
.await
.context("failed to load sender contact")?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
.await
.context("failed to load sender contact object")?;
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
let override_sender_name = message.get_override_sender_name();
@@ -136,9 +132,7 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::load_from_db(context, quote.get_from_id())
.await
.context("failed to load quote author contact")?;
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
Some(MessageQuote::WithMessage {
text: quoted_text,
message_id: quote.get_id().to_u32(),
@@ -166,9 +160,7 @@ impl MessageObject {
None
};
let reactions = get_msg_reactions(context, msg_id)
.await
.context("failed to load message reactions")?;
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = if reactions.is_empty() {
None
} else {
@@ -188,7 +180,7 @@ impl MessageObject {
state: message
.get_state()
.to_u32()
.context("state conversion to number failed")?,
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
error: message.error(),
timestamp: message.get_timestamp(),
@@ -211,7 +203,7 @@ impl MessageObject {
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.context("videochat type conversion to number failed")?,
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
),
None => None,
},
@@ -451,17 +443,9 @@ impl MessageNotificationInfo {
pub struct MessageSearchResult {
id: u32,
author_profile_image: Option<String>,
/// if sender name if overridden it will show it as ~alias
author_name: String,
author_color: String,
author_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
chat_type: u32,
is_chat_protected: bool,
is_chat_contact_request: bool,
is_chat_archived: bool,
chat_name: Option<String>,
message: String,
timestamp: i64,
}
@@ -476,31 +460,17 @@ impl MessageSearchResult {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let chat_profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let author_name = if let Some(name) = message.get_override_sender_name() {
format!("~{name}")
} else {
sender.get_display_name().to_owned()
};
let chat_color = color_int_to_hex_string(chat.get_color(context).await?);
Ok(Self {
id: msg_id.to_u32(),
author_profile_image: profile_image,
author_name,
author_name: sender.get_display_name().to_owned(),
author_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_profile_image,
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
chat_name: if chat.get_type() == Chattype::Single {
Some(chat.get_name().to_owned())
} else {
None
},
message: message.get_text().unwrap_or_default(),
timestamp: message.get_timestamp(),
})
@@ -544,10 +514,3 @@ pub struct MessageData {
pub override_sender_name: Option<String>,
pub quoted_message_id: Option<u32>,
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageReadReceipt {
pub contact_id: u32,
pub timestamp: i64,
}

View File

@@ -2,8 +2,6 @@ pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod events;
pub mod http;
pub mod location;
pub mod message;
pub mod provider_info;

View File

@@ -1,37 +1,23 @@
use std::collections::BTreeMap;
use deltachat::contact::ContactId;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
/// A single reaction emoji.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reaction", rename_all = "camelCase")]
pub struct JSONRPCReaction {
/// Emoji.
emoji: String,
/// Emoji frequency.
count: usize,
/// True if we reacted with this emoji.
is_from_self: bool,
}
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JSONRPCReaction>,
/// Unique reactions and their count
reactions: BTreeMap<String, u32>,
}
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
@@ -44,29 +30,18 @@ impl From<Reactions> for JSONRPCReactions {
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
}
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
let mut reactions_v = Vec::new();
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
let is_from_self = if let Some(self_reactions) = self_reactions {
self_reactions.contains(&emoji)
} else {
false
};
let reaction = JSONRPCReaction {
emoji,
count,
is_from_self,
};
reactions_v.push(reaction)
for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
*x += 1;
} else {
unique_reactions.insert(emoji, 1);
}
}
}
JSONRPCReactions {
reactions_by_contact,
reactions: reactions_v,
reactions: unique_reactions,
}
}
}

View File

@@ -1,4 +1,5 @@
pub mod api;
pub use api::events;
pub use yerpc;
#[cfg(test)]

View File

@@ -6,6 +6,7 @@ use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};
mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi};
const DEFAULT_PORT: u16 = 20808;
@@ -43,5 +44,12 @@ async fn main() -> Result<(), std::io::Error> {
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
tokio::spawn(async move {
let events = api.accounts.read().await.get_event_emitter();
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await.ok();
}
});
handle_ws_rpc(ws, out_receiver, session).await
}

View File

@@ -67,7 +67,7 @@ async function run() {
null,
null
);
for (const chatId of chats) {
for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.getMessageIds(

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.115.0"
"version": "1.111.0"
}

View File

@@ -1,28 +1,34 @@
import * as T from "../generated/types.js";
import { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { Event } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["type"]]: (
type DCWireEvent<T extends Event> = {
event: T;
contextId: number;
};
// export type Events = Record<
// Event["type"] | "ALL",
// (event: DeltaChatEvent<Event>) => void
// >;
type Events = { ALL: (accountId: number, event: Event) => void } & {
[Property in Event["type"]]: (
accountId: number,
event: Extract<EventType, { type: Property }>
event: Extract<Event, { type: Property }>
) => void;
};
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["type"]]: (
event: Extract<EventType, { type: Property }>
type ContextEvents = { ALL: (event: Event) => void } & {
[Property in Event["type"]]: (
event: Extract<Event, { type: Property }>
) => void;
};
export type DcEvent = EventType;
export type DcEventType<T extends EventType["type"]> = Extract<
EventType,
{ type: T }
>;
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
@@ -30,34 +36,27 @@ export class BaseDeltaChat<
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
//@ts-ignore
private eventTask: Promise<void>;
constructor(public transport: Transport, startEventLoop: boolean) {
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
if (startEventLoop) {
this.eventTask = this.eventLoop();
}
}
this.transport.on("request", (request: Request) => {
const method = request.method;
if (method === "event") {
const event = request.params! as DCWireEvent<Event>;
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);
async eventLoop(): Promise<void> {
while (true) {
const event = await this.rpc.getNextEvent();
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event);
this.emit("ALL", event.contextId, event.event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
//@ts-ignore
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
//@ts-ignore
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.event);
}
}
}
});
}
async listAccounts(): Promise<T.Account[]> {
@@ -76,12 +75,10 @@ export class BaseDeltaChat<
export type Opts = {
url: string;
startEventLoop: boolean;
};
export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
startEventLoop: true,
};
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
opts: Opts;
@@ -89,24 +86,20 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
this.transport.close();
}
constructor(opts?: Opts | string) {
if (typeof opts === "string") {
opts = { ...DEFAULT_OPTS, url: opts };
} else if (opts) {
opts = { ...DEFAULT_OPTS, ...opts };
} else {
opts = { ...DEFAULT_OPTS };
}
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
const transport = new WebsocketTransport(opts.url);
super(transport, opts.startEventLoop);
super(transport);
this.opts = opts;
}
}
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
close() {}
constructor(input: any, output: any, startEventLoop: boolean) {
constructor(input: any, output: any) {
const transport = new StdioTransport(input, output);
super(transport, startEventLoop);
super(transport);
}
}

View File

@@ -1,5 +1,6 @@
export * as RPC from "../generated/jsonrpc.js";
export * as T from "../generated/types.js";
export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";

View File

@@ -12,7 +12,7 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
// dc.on("ALL", (event) => {
//console.log("event", event);
// });

View File

@@ -27,7 +27,7 @@ describe("online tests", function () {
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.115.0"
version = "1.111.0"
license = "MPL-2.0"
edition = "2021"
@@ -8,10 +8,10 @@ edition = "2021"
ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
dirs = "4"
log = "0.4.16"
pretty_env_logger = "0.4"
rusqlite = "0.29"
rusqlite = "0.28"
rustyline = "11"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }

View File

@@ -563,7 +563,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await.log_err(&context).ok();
sql::housekeeping(&context).await.ok_or_log(&context);
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" {

View File

@@ -5,23 +5,9 @@ and provides asynchronous interface to it.
## Getting started
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`
or download a prebuilt release.
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
Install it anywhere in your `PATH`.
[Create a virtual environment](https://docs.python.org/3/library/venv.html)
if you don't have one already and activate it.
```
$ python -m venv env
$ . env/bin/activate
```
Install `deltachat-rpc-client` from source:
```
$ cd deltachat-rpc-client
$ pip install .
```
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.

View File

@@ -6,7 +6,7 @@ import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
from deltachat_rpc_client import DeltaChat, EventType, Rpc
async def main():
@@ -30,9 +30,9 @@ async def main():
await deltachat.start_io()
async def process_messages():
for message in await account.get_next_messages():
for message in await account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
if not snapshot.is_bot and not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()

View File

@@ -21,9 +21,6 @@ deltachat_rpc_client = [
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.setuptools_scm]
root = ".."
[tool.black]
line-length = 120

View File

@@ -3,7 +3,7 @@ from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat
from .client import Bot, Client
from .const import EventType, SpecialContactId
from .const import EventType
from .contact import Contact
from .deltachat import DeltaChat
from .message import Message
@@ -19,7 +19,6 @@ __all__ = [
"DeltaChat",
"EventType",
"Message",
"SpecialContactId",
"Rpc",
"run_bot_cli",
"run_client_cli",

View File

@@ -1,6 +1,5 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from warnings import warn
from ._utils import AttrDict
from .chat import Chat
@@ -177,7 +176,7 @@ class Account:
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
return [Chat(self, entry) for entry in entries]
return [Chat(self, entry[0]) for entry in entries]
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
@@ -240,22 +239,7 @@ class Account:
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
async def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = await self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
async def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]

View File

@@ -105,10 +105,6 @@ class Chat:
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
async def can_send(self) -> bool:
"""Return true if messages can be sent to the chat."""
return await self._rpc.can_send(self.account.id, self.id)
async def send_message(
self,
text: Optional[str] = None,
@@ -130,7 +126,7 @@ class Chat:
"file": file,
"location": location,
"overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg,
"quotedMsg": quoted_msg,
}
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
return Message(self.account, msg_id)

View File

@@ -20,7 +20,7 @@ from ._utils import (
parse_system_image_changed,
parse_system_title_changed,
)
from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType
from .const import COMMAND_PREFIX, EventType, SystemMessageType
from .events import (
EventFilter,
GroupImageChanged,
@@ -189,10 +189,9 @@ class Client:
async def _process_messages(self) -> None:
if self._should_process_messages:
for message in await self.account.get_next_messages():
for message in await self.account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
await self._on_new_msg(snapshot)
await self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen()

View File

@@ -31,7 +31,6 @@ class EventType(str, Enum):
SMTP_MESSAGE_SENT = "SmtpMessageSent"
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
IMAP_INBOX_IDLE = "ImapInboxIdle"
NEW_BLOB_FILE = "NewBlobFile"
DELETED_BLOB_FILE = "DeletedBlobFile"
WARNING = "Warning"

View File

@@ -1,6 +1,6 @@
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Union
from ._utils import AttrDict
from .contact import Contact
@@ -35,13 +35,6 @@ class Message:
snapshot["message"] = self
return snapshot
async def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
if reactions:
return AttrDict(reactions)
return None
async def mark_seen(self) -> None:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account.id, [self.id])

View File

@@ -55,11 +55,6 @@ class ACFactory:
async def get_online_account(self) -> Account:
account = await self.new_configured_account()
await account.start_io()
while True:
event = await account.wait_for_event()
print(event)
if event.type == EventType.IMAP_INBOX_IDLE:
break
return account
async def get_online_accounts(self, num: int) -> List[Account]:

View File

@@ -23,9 +23,7 @@ class Rpc:
self.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future]
self.closing: bool
self.reader_task: asyncio.Task
self.events_task: asyncio.Task
async def start(self) -> None:
self.process = await asyncio.create_subprocess_exec(
@@ -37,15 +35,10 @@ class Rpc:
self.id = 0
self.event_queues = {}
self.request_events = {}
self.closing = False
self.reader_task = asyncio.create_task(self.reader_loop())
self.events_task = asyncio.create_task(self.events_loop())
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
await self.stop_io_for_all_accounts()
await self.events_task
self.process.terminate()
await self.reader_task
@@ -65,28 +58,21 @@ class Rpc:
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
elif response["method"] == "event":
# An event notification.
params = response["params"]
account_id = params["contextId"]
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
await self.event_queues[account_id].put(params["event"])
else:
print(response)
async def get_queue(self, account_id: int) -> asyncio.Queue:
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
return self.event_queues[account_id]
async def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
while True:
if self.closing:
return
event = await self.get_next_event()
account_id = event["contextId"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
queue = await self.get_queue(account_id)
return await queue.get()
if account_id in self.event_queues:
return await self.event_queues[account_id].get()
return None
def __getattr__(self, attr: str):
async def method(*args, **kwargs) -> Any:

View File

@@ -98,8 +98,8 @@ async def test_account(acfactory) -> None:
assert await alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code()
assert await alice.get_fresh_messages()
assert await alice.get_next_messages()
await alice.get_fresh_messages()
await alice.get_fresh_messages_in_arrival_order()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
@@ -147,9 +147,7 @@ async def test_chat(acfactory) -> None:
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
await alice_chat_bob.delete()
assert not await bob_chat_alice.can_send()
await bob_chat_alice.accept()
assert await bob_chat_alice.can_send()
await bob_chat_alice.block()
bob_chat_alice = await snapshot.sender.create_chat()
await bob_chat_alice.mute()
@@ -239,10 +237,6 @@ async def test_message(acfactory) -> None:
await message.mark_seen()
await message.send_reaction("😎")
reactions = await message.get_reactions()
assert reactions
snapshot = await message.get_snapshot()
assert reactions == snapshot.reactions
@pytest.mark.asyncio()
@@ -309,29 +303,3 @@ async def test_bot(acfactory) -> None:
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
@pytest.mark.asyncio()
async def test_wait_next_messages(acfactory) -> None:
alice = await acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
bot = await acfactory.new_preconfigured_account()
await bot.set_config("bot", "1")
await bot.configure()
# There are no old messages and the call returns immediately.
assert not await bot.wait_next_messages()
# Bot starts waiting for messages.
next_messages_task = asyncio.create_task(bot.wait_next_messages())
bot_addr = await bot.get_config("addr")
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
alice_chat_bot = await alice_contact_bot.create_chat()
await alice_chat_bot.send_text("Hello!")
next_messages = await next_messages_task
assert len(next_messages) == 1
snapshot = await next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"

View File

@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
pytest {posargs}
pytest --exitfirst {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
@@ -15,7 +15,6 @@ passenv =
deps =
pytest
pytest-asyncio
pytest-timeout
aiohttp
aiodns
@@ -28,6 +27,3 @@ deps =
commands =
black --quiet --check --diff src/ examples/ tests/
ruff src/ examples/ tests/
[pytest]
timeout = 60

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.115.0"
version = "1.111.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -9,18 +9,19 @@ license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
[[bin]]
name = "deltachat-rpc-server"
[dependencies]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
futures-lite = "1.12.0"
log = "0.4"
serde_json = "1.0.96"
serde_json = "1.0.91"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.28.0", features = ["io-std"] }
tokio-util = "0.7.8"
tokio = { version = "1.25.0", features = ["io-std"] }
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
[features]

View File

@@ -0,0 +1,71 @@
///! Delta Chat core RPC server.
///!
///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use anyhow::Result;
use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tokio::task::JoinHandle;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
let events = accounts.get_event_emitter();
log::info!("Creating JSON-RPC API.");
let state = CommandApi::new(accounts);
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state);
// Events task converts core events to JSON-RPC notifications.
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await?;
}
Ok(())
});
// Send task prints JSON responses to stdout.
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
while let Some(message) = out_receiver.next().await {
let message = serde_json::to_string(&message)?;
log::trace!("RPC send {}", message);
println!("{message}");
}
Ok(())
});
// Receiver task reads JSON requests from stdin.
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let stdin = io::stdin();
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
log::trace!("RPC recv {}", message);
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
});
}
log::info!("EOF reached on stdin");
Ok(())
});
// Wait for the end of stdin.
recv_task.await??;
// Shutdown the server.
send_task.abort();
events_task.abort();
Ok(())
}

View File

@@ -1,143 +0,0 @@
use std::env;
///! Delta Chat core RPC server.
///!
///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Context as _, Result};
use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
#[cfg(target_family = "unix")]
use tokio::signal::unix as signal_unix;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let r = main_impl().await;
// From tokio documentation:
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
// until the user presses enter."
std::process::exit(if r.is_ok() { 0 } else { 1 });
}
async fn main_impl() -> Result<()> {
let mut args = env::args_os();
let _program_name = args.next().context("no command line arguments found")?;
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));
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
}
}
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
}
// Install signal handlers early so that the shutdown is graceful starting from here.
let _ctrl_c = tokio::signal::ctrl_c();
#[cfg(target_family = "unix")]
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));
let state = CommandApi::from_arc(accounts.clone());
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state.clone());
let main_cancel = CancellationToken::new();
// Send task prints JSON responses to stdout.
let cancel = main_cancel.clone();
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let _cancel_guard = cancel.clone().drop_guard();
loop {
let message = tokio::select! {
_ = cancel.cancelled() => break,
message = out_receiver.next() => match message {
None => break,
Some(message) => serde_json::to_string(&message)?,
}
};
log::trace!("RPC send {}", message);
println!("{message}");
}
Ok(())
});
let cancel = main_cancel.clone();
let sigterm_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
#[cfg(target_family = "unix")]
{
let _cancel_guard = cancel.clone().drop_guard();
tokio::select! {
_ = cancel.cancelled() => (),
_ = sigterm.recv() => {
log::info!("got SIGTERM");
}
}
}
let _ = cancel;
Ok(())
});
// Receiver task reads JSON requests from stdin.
let cancel = main_cancel.clone();
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let _cancel_guard = cancel.clone().drop_guard();
let stdin = io::stdin();
let mut lines = BufReader::new(stdin).lines();
loop {
let message = tokio::select! {
_ = cancel.cancelled() => break,
_ = tokio::signal::ctrl_c() => {
log::info!("got ctrl-c event");
break;
}
message = lines.next_line() => match message? {
None => {
log::info!("EOF reached on stdin");
break;
}
Some(message) => message,
}
};
log::trace!("RPC recv {}", message);
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
});
}
Ok(())
});
main_cancel.cancelled().await;
accounts.read().await.stop_io().await;
drop(accounts);
drop(state);
send_task.await??;
sigterm_task.await??;
recv_task.await??;
Ok(())
}

View File

@@ -8,5 +8,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "2"
syn = "1"
quote = "1"

View File

@@ -11,54 +11,42 @@ ignore = [
# Accept some duplicate versions, ideally we work towards this list
# becoming empty. Adding versions forces us to revisit this at least
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling", version = "<0.14" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "env_logger", version = "<0.10" },
{ name = "getrandom", version = "<0.2" },
{ name = "hermit-abi", version = "<0.3" },
{ name = "humantime", version = "<2.1" },
{ name = "idna", version = "<0.3" },
{ name = "libm", version = "0.1.4" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "nom", version = "<7.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand", version = "<0.8" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "version_check", version = "<0.9" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
{ name = "windows_aarch64_msvc", version = "<0.48" },
{ name = "windows_i686_gnu", version = "<0.48" },
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.45" },
{ name = "windows_x86_64_msvc", version = "<0.42" },
{ name = "windows_x86_64_gnu", version = "<0.42" },
{ name = "windows_i686_msvc", version = "<0.42" },
{ name = "windows_i686_gnu", version = "<0.42" },
{ name = "windows_aarch64_msvc", version = "<0.42" },
{ name = "unicode-xid", version = "<0.2.4" },
{ name = "syn", version = "<1.0" },
{ name = "quote", version = "<1.0" },
{ name = "proc-macro2", version = "<1.0" },
{ name = "portable-atomic", version = "<1.0" },
{ name = "spin", version = "<0.9.6" },
{ name = "convert_case", version = "0.4.0" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
]
@@ -90,5 +78,7 @@ license-files = [
github = [
"async-email",
"deltachat",
"n0-computer",
"quinn-rs",
"dignifiedquire",
]

1803
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,6 @@ module.exports = {
DC_EVENT_ERROR: 400,
DC_EVENT_ERROR_SELF_NOT_IN_GROUP: 410,
DC_EVENT_IMAP_CONNECTED: 102,
DC_EVENT_IMAP_INBOX_IDLE: 106,
DC_EVENT_IMAP_MESSAGE_DELETED: 104,
DC_EVENT_IMAP_MESSAGE_MOVED: 105,
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
@@ -113,7 +112,6 @@ module.exports = {
DC_QR_ADDR: 320,
DC_QR_ASK_VERIFYCONTACT: 200,
DC_QR_ASK_VERIFYGROUP: 202,
DC_QR_BACKUP: 251,
DC_QR_ERROR: 400,
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
@@ -151,8 +149,6 @@ module.exports = {
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
DC_STR_BACKUP_TRANSFER_MSG_BODY: 163,
DC_STR_BACKUP_TRANSFER_QR: 162,
DC_STR_BAD_TIME_MSG_BODY: 85,
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,

View File

@@ -8,7 +8,6 @@ module.exports = {
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
106: 'DC_EVENT_IMAP_INBOX_IDLE',
150: 'DC_EVENT_NEW_BLOB_FILE',
151: 'DC_EVENT_DELETED_BLOB_FILE',
300: 'DC_EVENT_WARNING',

View File

@@ -37,7 +37,6 @@ export enum C {
DC_EVENT_ERROR = 400,
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
DC_EVENT_IMAP_CONNECTED = 102,
DC_EVENT_IMAP_INBOX_IDLE = 106,
DC_EVENT_IMAP_MESSAGE_DELETED = 104,
DC_EVENT_IMAP_MESSAGE_MOVED = 105,
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
@@ -113,7 +112,6 @@ export enum C {
DC_QR_ADDR = 320,
DC_QR_ASK_VERIFYCONTACT = 200,
DC_QR_ASK_VERIFYGROUP = 202,
DC_QR_BACKUP = 251,
DC_QR_ERROR = 400,
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
@@ -151,8 +149,6 @@ export enum C {
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
DC_STR_BACKUP_TRANSFER_QR = 162,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
@@ -293,7 +289,6 @@ export const EventId2EventName: { [key: number]: string } = {
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
106: 'DC_EVENT_IMAP_INBOX_IDLE',
150: 'DC_EVENT_NEW_BLOB_FILE',
151: 'DC_EVENT_DELETED_BLOB_FILE',
300: 'DC_EVENT_WARNING',

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.115.0"
}
"version": "1.111.0"
}

View File

@@ -12,17 +12,17 @@ a low-level Chat/Contact/Message API to user interfaces and bots.
Installing pre-built packages (Linux-only)
==========================================
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
We recommend to first create a fresh Python virtual environment
and activate it in your shell::
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
then create a fresh Python virtual environment and activate it in your shell::
python -m venv env
source env/bin/activate
virtualenv env # or: python -m venv
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
@@ -40,14 +40,16 @@ To verify it worked::
Running tests
=============
Recommended way to run tests is using `scripts/run-python-test.sh`
script provided in the core repository.
Recommended way to run tests is using `tox <https://tox.wiki>`_.
After successful binding installation you can install tox
and run the tests::
This script compiles the library in debug mode and runs the tests using `tox`_.
By default it will run all "offline" tests and skip all functional
pip install tox
tox -e py3
This will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _`tox`: https://tox.wiki
.. _livetests:
Running "live" tests with temporary accounts
@@ -59,34 +61,13 @@ Please feel free to contact us through a github issue or by e-mail and we'll sen
export DCC_NEW_TMP_EMAIL=<URL you got from us>
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts are removed automatically as they expire.
After setting the variable, either rerun `scripts/run-python-test.sh`
or run offline and online tests with `tox` directly::
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests::
tox -e py
tox -e py3
Each test run creates new accounts.
Developing the bindings
-----------------------
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=debug
tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed
in editable mode (as if installed with `python -m pip install -e`)
together with the testing dependencies like `pytest` and its plugins.
You can then edit the source code in the development tree
and quickly run `pytest` manually without waiting for `tox`
to recreating the virtual environment each time.
.. _sourceinstall:
Installing bindings from source
@@ -108,34 +89,20 @@ To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
First, build the core library::
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment with dependencies using tox
and activate it in your shell::
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it:
python -m venv env
cd python
tox --devenv env
source env/bin/activate
Build and install the bindings:
You should now be able to build the python bindings using the supplied script::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=release
python -m pip install ./python
python3 install_python_bindings.py
`DCC_RS_DEV` environment variable specifies the location of
the core development tree. If this variable is not set,
`libdeltachat` library and `deltachat.h` header are expected
to be installed system-wide.
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
the build profile name to look up the artifacts
in the target directory.
In this case setting it can be skipped because
`DCC_RS_TARGET=release` is the default.
The core compilation and bindings building might take a while,
depending on the speed of your machine.
Building manylinux based wheels
===============================

View File

@@ -24,8 +24,6 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating a temp account to contact the bot")
(ac1,) = acfactory.get_online_accounts(1)
botproc.await_resync()
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
bot_chat = bot_contact.create_chat()
@@ -42,7 +40,7 @@ def test_echo_quit_plugin(acfactory, lp):
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking)
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -54,8 +52,6 @@ def test_group_tracking_plugin(acfactory, lp):
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
botproc.await_resync()
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""
setup a python binding development in-place install with cargo debug symbols.
"""
import os
import subprocess
import sys
if __name__ == "__main__":
target = os.environ.get("DCC_RS_TARGET")
if target is None:
os.environ["DCC_RS_TARGET"] = target = "debug"
if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
if target == "release":
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
cmd.append("--release")
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll", shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", "."])

View File

@@ -376,22 +376,6 @@ class Account:
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
def _wait_next_message_ids(self) -> List[int]:
"""Return IDs of all next messages from all chats."""
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
def wait_next_incoming_message(self) -> Message:
"""Waits until the next incoming message
with ID higher than given is received and returns it."""
while True:
message_ids = self._wait_next_message_ids()
for msg_id in message_ids:
message = Message.from_db(self, msg_id)
if message and not message.is_from_self() and not message.is_from_device():
self.set_config("last_msg_id", str(msg_id))
return message
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat()

View File

@@ -190,7 +190,7 @@ class FFIEventTracker:
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent
"""
self.get_matching("DC_EVENT_IMAP_INBOX_IDLE")
self.get_info_contains("INBOX: Idle entering")
def wait_next_incoming_message(self):
"""wait for and return next incoming message."""

View File

@@ -344,16 +344,6 @@ class Message:
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self.account, contact_id)
def is_from_self(self):
"""Return true if the message is sent by self."""
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return contact_id == const.DC_CONTACT_ID_SELF
def is_from_device(self):
"""Return true if the message is sent by the device."""
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return contact_id == const.DC_CONTACT_ID_DEVICE
#
# Message State query methods
#

View File

@@ -676,13 +676,6 @@ class BotProcess:
print("+++IGN:", line)
ignored.append(line)
def await_resync(self):
self.fnmatch_lines(
"""
*Resync: collected * message IDs in folder INBOX*
""",
)
@pytest.fixture()
def tmp_db_path(tmpdir):

View File

@@ -0,0 +1,19 @@
import os
import subprocess
import sys
if __name__ == "__main__":
assert len(sys.argv) == 2
workspacedir = sys.argv[1]
for relpath in os.listdir(workspacedir):
if relpath.startswith("deltachat"):
p = os.path.join(workspacedir, relpath)
subprocess.check_call(
[
"auditwheel",
"repair",
p,
"-w",
workspacedir,
],
)

View File

@@ -220,16 +220,16 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
acfactory.bring_accounts_online()
assert_folders_configured(ac1)
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
assert ac1.direct_imap.select_config_folder("mvbox" if mvbox_move else "inbox")
with ac1.direct_imap.idle() as idle1:
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
assert_folders_configured(ac1)
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
if mvbox_move:
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
assert idle1.wait_for_seen()
assert_folders_configured(ac1)
lp.sec("create a cloned ac1 and fetch contact history during configure")
@@ -271,12 +271,12 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
ac1._evtracker.wait_next_incoming_message()
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
# wait until the bcc_self message arrives
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
with ac1.direct_imap.idle() as idle1:
ac1.set_config("bcc_self", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
# wait until the bcc_self message arrives
assert idle1.wait_for_seen()
lp.sec("Clone online account and let it fetch the existing messages")
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)

View File

@@ -44,21 +44,21 @@ def test_configure_generate_key(acfactory, lp):
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2.wait_next_incoming_message()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1.wait_next_incoming_message()
msg2_in = ac1._evtracker.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2.wait_next_incoming_message()
msg3_in = ac2._evtracker.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
@@ -134,27 +134,22 @@ def test_one_account_send_bcc_setting(acfactory, lp):
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
with ac1.direct_imap.idle() as idle1:
msg_out = chat.send_text("message2")
# wait for send out (BCC)
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# wait for send out (BCC)
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
# now make sure we are sending message to ourselves too
assert self_addr in ev.data2
assert other_addr in ev.data2
assert idle1.wait_for_seen()
# Second client receives only second message, but not the first
ev_msg = ac1_clone._evtracker.wait_next_messages_changed()
assert ev_msg.text == msg_out.text
# now make sure we are sending message to ourselves too
assert self_addr in ev.data2
assert other_addr in ev.data2
# BCC-self messages are marked as seen by the sender device.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
# Check that the message is marked as seen on IMAP.
ac1.direct_imap.select_folder("Inbox")
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -319,9 +314,6 @@ def test_webxdc_message(acfactory, data, lp):
assert msg2.text == "message1"
assert msg2.is_webxdc()
assert msg2.filename
ac2._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
ac2.direct_imap.select_folder("Inbox")
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_mvbox_sentbox_threads(acfactory, lp):
@@ -521,22 +513,22 @@ def test_send_and_receive_message_markseen(acfactory, lp):
msg4 = ac2._evtracker.wait_next_incoming_message()
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg2, msg4])
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert msg2.chat.id == msg4.chat.id
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
ac2._evtracker.get_info_contains("Marked messages .* in folder INBOX as seen.")
with ac1.direct_imap.idle() as idle1:
with ac2.direct_imap.idle() as idle2:
ac2.mark_seen_messages([msg2, msg4])
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert msg2.chat.id == msg4.chat.id
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
idle2.wait_for_seen()
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
# Check that ac1 marks the read receipt as read.
ac1._evtracker.get_info_contains("Marked messages .* in folder INBOX as seen.")
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
idle1.wait_for_seen() # Check that ac1 marks the read receipt as read
assert msg1.is_out_mdn_received()
assert msg3.is_out_mdn_received()
@@ -567,8 +559,7 @@ def test_moved_markseen(acfactory):
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
msg = ac2._evtracker.wait_next_incoming_message()
# Accept the contact request.
msg.chat.accept()
@@ -622,24 +613,18 @@ def test_markseen_message_and_mdn(acfactory, mvbox_move):
# Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
ac2.mark_seen_messages([msg])
folder = "mvbox" if mvbox_move else "inbox"
for ac in [ac1, ac2]:
if mvbox_move:
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
ac1.direct_imap.select_config_folder(folder)
ac2.direct_imap.select_config_folder(folder)
with ac1.direct_imap.idle() as idle1:
with ac2.direct_imap.idle() as idle2:
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
# Check that the mdn is marked as seen
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
# Check original message is marked as seen
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
ac2.mark_seen_messages([msg])
idle2.wait_for_seen() # Check original message is marked as seen
idle1.wait_for_seen() # Check that the mdn is marked as seen
def test_reply_privately(acfactory):
@@ -693,24 +678,23 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
ac1.direct_imap.select_config_folder("mvbox")
with ac1.direct_imap.idle() as idle1:
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
lp.sec("ac1: waiting for incoming activity")
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
lp.sec("ac1: waiting for incoming activity")
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
# Wait for the message to be marked as seen on IMAP.
assert idle1.wait_for_seen()
# MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received()
ac1.direct_imap.select_config_folder("mvbox")
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)

View File

@@ -9,7 +9,6 @@ from deltachat.testplugin import (
create_dict_from_files_in_path,
write_dict_to_dir,
)
from deltachat.cutil import from_optional_dc_charpointer
# from deltachat.account import EventLogger
@@ -216,19 +215,3 @@ def test_logged_ac_process_ffi_failure(acfactory):
assert "ac_process_ffi_event" in res
assert "ZeroDivisionError" in res
assert "Traceback" in res
def test_jsonrpc_blocking_call(tmpdir):
accounts_fname = tmpdir.join("accounts")
accounts = ffi.gc(
lib.dc_accounts_new(ffi.NULL, accounts_fname.strpath.encode("ascii")),
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == "true"
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

@@ -8,7 +8,7 @@ envlist =
[testenv]
commands =
pytest -n6 --extra-info -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --exitfirst --extra-info -v -rsXx --ignored --strict-tls {posargs: tests examples}
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
@@ -33,6 +33,18 @@ passenv =
CARGO_TARGET_DIR
RUSTC_WRAPPER
[testenv:auditwheels]
skipsdist = True
deps = auditwheel
passenv =
DCC_RS_DEV
DCC_RS_TARGET
AUDITWHEEL_ARCH
AUDITWHEEL_PLAT
AUDITWHEEL_POLICY
commands =
python tests/auditwheels.py {toxworkdir}/wheelhouse
[testenv:lint]
skipsdist = True
skip_install = True
@@ -43,7 +55,7 @@ deps =
pygments
restructuredtext_lint
commands =
black --quiet --check --diff setup.py src/deltachat examples/ tests/
black --quiet --check --diff setup.py install_python_bindings.py src/deltachat examples/ tests/
ruff src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst

View File

@@ -1 +0,0 @@
2023-05-12

3
release.toml Normal file
View File

@@ -0,0 +1,3 @@
pre-release-commit-message = "chore({{crate_name}}): release {{version}}"
pro-release-commit-message = "chore({{crate_name}}): starting development cycle for {{next_version}}"
no-dev-version = true

View File

@@ -8,8 +8,6 @@ and an own build machine.
- `clippy.sh` runs `cargo clippy` for all Rust code in the project.
- `deny.sh` runs `cargo deny` for all Rust code in the project.
- `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs

View File

@@ -233,7 +233,7 @@ if __name__ == "__main__":
else:
now = datetime.datetime.fromisoformat(sys.argv[2])
out_all += (
"pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
"pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
+ str(now.year)
+ ", "

View File

@@ -1,6 +0,0 @@
#!/bin/sh
# Update package cache without changing the lockfile.
cargo update --dry-run
cargo deny --workspace --all-features check -D warnings

View File

@@ -12,7 +12,7 @@ export DCC_RS_DEV=`pwd`
cd python
cargo build -p deltachat_ffi --features jsonrpc
python install_python_bindings.py onlybuild
# remove and inhibit writing PYC files
rm -rf tests/__pycache__
@@ -22,5 +22,4 @@ export PYTHONDONTWRITEBYTECODE=1
# run python tests (tox invokes pytest to run tests in python/tests)
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
tox -e lint
tox -e doc
tox -e py -- "$@"
tox -e doc,py3

View File

@@ -31,9 +31,7 @@ unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
echo -----------------------

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python3
import datetime
import json
import os
import pathlib
import re
import subprocess
from argparse import ArgumentParser
from pathlib import Path
rex = re.compile(r'version = "(\S+)"')
@@ -58,8 +56,7 @@ def update_package_json(relpath, newversion):
json_data = json.loads(f.read())
json_data["version"] = newversion
with open(p, "w") as f:
json.dump(json_data, f, sort_keys=True, indent=2)
f.write("\n")
f.write(json.dumps(json_data, sort_keys=True, indent=2))
def main():
@@ -93,16 +90,14 @@ def main():
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
today = datetime.date.today().isoformat()
if "alpha" not in newversion:
found = False
for line in Path("CHANGELOG.md").open():
if line == f"## [{newversion}] - {today}\n":
found = True
if not found:
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## ") and line[2:].strip().startswith(newversion):
break
else:
raise SystemExit(
f"{changelog_name} contains no entry for version: {newversion}"
f"CHANGELOG.md contains no entry for version: {newversion}"
)
for toml_filename in toml_list:
@@ -111,9 +106,6 @@ def main():
for json_filename in json_list:
update_package_json(json_filename, newversion)
with open("release-date.in", "w") as f:
f.write(today)
print("running cargo check")
subprocess.call(["cargo", "check"])
@@ -121,15 +113,11 @@ def main():
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
print("After commit, make sure to:")
print()
print("after commit, on master make sure to: ")
print("")
print(f" git tag -a v{newversion}")
print(f" git push origin v{newversion}")
print(f" gh release create v{newversion} -n ''")
print()
print("Merge release branch into `master` if the release")
print("is made on a stable branch.")
print()
print("")
if __name__ == "__main__":

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env python
import subprocess
import sys
import os
def flag_filter(flag: str) -> bool:
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
return False
if "self-contained" in flag and "crt" in flag:
return False
return True
def main():
args = [flag for flag in sys.argv[1:] if flag_filter(flag)]
zig_target = os.environ["ZIG_TARGET"]
zig_cpu = os.environ.get("ZIG_CPU")
if zig_cpu:
zig_cpu_args = ["-mcpu=" + zig_cpu]
args = [x for x in args if not x.startswith("-march")]
else:
zig_cpu_args = []
subprocess.run(
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
)
main()

View File

@@ -1,15 +1,12 @@
#!/bin/sh
#
# Build statically linked deltachat-rpc-server using zig.
# Build statically linked deltachat-rpc-server using cargo-zigbuild.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.68.1
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
@@ -18,35 +15,9 @@ wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86-linux-musl" \
cargo build --release --target i686-unknown-linux-musl -p deltachat-rpc-server --features vendored
cargo install cargo-zigbuild
rustup target add armv7-unknown-linux-musleabihf
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="arm-linux-musleabihf" \
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
cargo build --release --target armv7-unknown-linux-musleabihf -p deltachat-rpc-server --features vendored
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
rustup target add aarch64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
for TARGET in x86_64-unknown-linux-musl aarch64-unknown-linux-musl armv7-unknown-linux-musleabihf; do
rustup target add "$TARGET"
cargo zigbuild --release --target "$TARGET" -p deltachat-rpc-server --features vendored
done

View File

@@ -6,7 +6,6 @@ use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
use crate::context::Context;
@@ -18,7 +17,6 @@ use crate::stock_str::StockStrings;
pub struct Accounts {
dir: PathBuf,
config: Config,
/// Map from account ID to the account.
accounts: BTreeMap<u32, Context>,
/// Event channel to emit account manager errors.
@@ -79,12 +77,12 @@ impl Accounts {
})
}
/// Returns an account by its `id`:
/// Get an account by its `id`:
pub fn get_account(&self, id: u32) -> Option<Context> {
self.accounts.get(&id).cloned()
}
/// Returns the currently selected account.
/// Get the currently selected account.
pub fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account();
self.accounts.get(&id).cloned()
@@ -98,14 +96,14 @@ impl Accounts {
}
}
/// Selects the given account.
/// Select the given account.
pub async fn select_account(&mut self, id: u32) -> Result<()> {
self.config.select_account(id).await?;
Ok(())
}
/// Adds a new account and opens it.
/// Add a new account and opens it.
///
/// Returns account ID.
pub async fn add_account(&mut self) -> Result<u32> {
@@ -140,7 +138,7 @@ impl Accounts {
Ok(account_config.id)
}
/// Removes an account.
/// Remove an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self
.accounts
@@ -161,7 +159,7 @@ impl Accounts {
Ok(())
}
/// Migrates an existing account into this structure.
/// Migrate an existing account into this structure.
///
/// Returns the ID of new account.
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
@@ -264,7 +262,7 @@ impl Accounts {
pub async fn stop_io(&self) {
// Sending an event here wakes up event loop even
// if there are no accounts.
info!(self, "Stopping IO for all accounts.");
info!(self, "Stopping IO for all accounts");
for account in self.accounts.values() {
account.stop_io().await;
}
@@ -303,7 +301,7 @@ pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone, PartialEq)]
struct Config {
pub struct Config {
file: PathBuf,
inner: InnerConfig,
}
@@ -327,8 +325,10 @@ impl Config {
selected_account: 0,
next_id: 1,
};
let file = dir.join(CONFIG_NAME);
let mut cfg = Self { file, inner };
let cfg = Config {
file: dir.join(CONFIG_NAME),
inner,
};
cfg.sync().await?;
@@ -336,24 +336,10 @@ impl Config {
}
/// Sync the inmemory representation to disk.
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
async fn sync(&self) -> Result<()> {
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
.await
.context("failed to create a tmp config")?;
file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
.await
.context("failed to write a tmp config")?;
file.sync_data()
.await
.context("failed to sync a tmp config")?;
drop(file);
fs::rename(&tmp_path, &self.file)
.await
.context("failed to rename config")?;
Ok(())
.context("failed to write config")
}
/// Read a configuration from the given file into memory.
@@ -373,7 +359,7 @@ impl Config {
}
}
let mut config = Self { file, inner };
let config = Self { file, inner };
if modified {
config.sync().await?;
}
@@ -517,19 +503,17 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts1");
{
let mut accounts = Accounts::new(p.clone()).await.unwrap();
accounts.add_account().await.unwrap();
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
accounts1.add_account().await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
{
let accounts = Accounts::open(p).await.unwrap();
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
assert_eq!(accounts1.accounts.len(), 1);
assert_eq!(accounts1.config.get_selected_account(), 1);
assert_eq!(accounts1.dir, accounts2.dir);
assert_eq!(accounts1.config, accounts2.config,);
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -308,7 +308,7 @@ async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i6
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
(from_domain,),
paramsv![from_domain],
)
.await?
.unwrap_or(0);
@@ -325,7 +325,7 @@ async fn set_dkim_works_timestamp(
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
(from_domain, timestamp),
paramsv![from_domain, timestamp],
)
.await?;
Ok(())

View File

@@ -9,17 +9,21 @@ use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use futures::StreamExt;
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
use image::{DynamicImage, ImageFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
use tokio_stream::wrappers::ReadDirStream;
use crate::config::Config;
use crate::constants::{self, MediaQuality};
use crate::constants::{
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message;
use crate::message::Viewtype;
/// Represents a file in the blob directory.
///
@@ -88,7 +92,7 @@ impl<'a> BlobObject<'a> {
if attempt >= MAX_ATTEMPT {
return Err(err).context("failed to create file");
} else if attempt == 1 && !dir.exists() {
fs::create_dir_all(dir).await.log_err(context).ok();
fs::create_dir_all(dir).await.ok_or_log(context);
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
}
@@ -319,149 +323,119 @@ impl<'a> BlobObject<'a> {
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
MediaQuality::Worse => WORSE_AVATAR_SIZE,
};
let strict_limits = true;
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
{
if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
self.name = new_name;
}
Ok(())
}
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
pub async fn recode_to_image_size(&self, context: &Context) -> Result<()> {
let blob_abs = self.to_abs_path();
let (img_wh, max_bytes) =
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
!= Some((Viewtype::Image, "image/jpeg"))
{
return Ok(());
}
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => (
constants::BALANCED_IMAGE_SIZE,
constants::BALANCED_IMAGE_BYTES,
),
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
MediaQuality::Worse => WORSE_IMAGE_SIZE,
};
let strict_limits = false;
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
if self
.recode_to_size(context, blob_abs, img_wh, None)?
.is_some()
{
self.name = new_name;
return Err(format_err!(
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
));
}
Ok(())
}
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
/// proceed with the result.
fn recode_to_size(
&self,
context: &Context,
mut blob_abs: PathBuf,
mut img_wh: u32,
max_bytes: usize,
strict_limits: bool,
max_bytes: Option<usize>,
) -> Result<Option<String>> {
tokio::task::block_in_place(move || {
let mut img = image::open(&blob_abs).context("image decode failure")?;
let (nr_bytes, exif) = self.metadata()?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut img = image::open(&blob_abs).context("image recode failure")?;
let orientation = self.get_exif_orientation(context);
let mut encoded = Vec::new();
let mut changed_name = None;
img = match orientation {
Some(90) => img.rotate90(),
Some(180) => img.rotate180(),
Some(270) => img.rotate270(),
_ => img,
};
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
_ => {
let jpeg_quality = 75;
ImageOutputFormat::Jpeg(jpeg_quality)
if do_scale || do_rotate {
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
}
}
};
// We need to rewrite images with Exif to remove metadata such as location,
// camera model, etc.
//
// TODO: Fix lost animation and transparency when recoding using the `image` crate. And
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
// images.
let do_scale = exceeds_max_bytes
|| strict_limits
&& (exceeds_wh
|| exif.is_some()
&& encoded_img_exceeds_bytes(
if do_scale {
if !exceeds_width {
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
}
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
));
}
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
&img,
ofmt.clone(),
max_bytes,
&mut encoded,
)?);
if do_scale {
if !exceeds_wh {
img_wh = max(img.width(), img.height());
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
// crate when recoding, so don't scale them down.
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
img_wh = img_wh * 2 / 3;
}
}
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(
context,
&new_img,
ofmt.clone(),
max_bytes,
&mut encoded,
)? && strict_limits
{
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B.",
max_bytes,
));
"Final scaled-down image size: {}B ({}px)",
encoded.len(),
img_wh
);
break;
}
img_wh = img_wh * 2 / 3;
} else {
info!(
context,
"Final scaled-down image size: {}B ({}px).",
encoded.len(),
img_wh
);
break;
}
}
}
if do_scale || exif.is_some() {
// The file format is JPEG/PNG now, we may have to change the file extension
if !matches!(fmt, Ok(ImageFormat::Jpeg))
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
{
// The file format is JPEG now, we may have to change the file extension
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No image file name (???)")?;
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
changed_name = Some(format!("$BLOBDIR/{file_name}"));
}
if encoded.is_empty() {
encode_img(&img, ofmt, &mut encoded)?;
encode_img(&img, &mut encoded)?;
}
std::fs::write(&blob_abs, &encoded)
@@ -472,28 +446,23 @@ impl<'a> BlobObject<'a> {
})
}
/// Returns image file size and Exif.
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32> {
let file = std::fs::File::open(self.to_abs_path())?;
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(&file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return 180,
Some(6) => return 90,
Some(8) => return 270,
other => warn!(context, "Exif orientation value ignored: {other:?}."),
let exifreader = exif::Reader::new();
let exif = exifreader.read_from_container(&mut bufreader)?;
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return Ok(180),
Some(6) => return Ok(90),
Some(8) => return Ok(270),
other => warn!(context, "exif orientation value ignored: {:?}", other),
}
}
Ok(0)
}
0
}
impl<'a> fmt::Display for BlobObject<'a> {
@@ -521,7 +490,7 @@ impl<'a> BlobDirContents<'a> {
match entry {
Ok(entry) => Some(entry),
Err(err) => {
error!(context, "Failed to read blob file: {err}.");
error!(context, "Failed to read blob file: {err}");
None
}
}
@@ -532,7 +501,7 @@ impl<'a> BlobDirContents<'a> {
false => {
warn!(
context,
"Export: Found blob dir entry {} that is not a file, ignoring.",
"Export: Found blob dir entry {} that is not a file, ignoring",
entry.path().display()
);
None
@@ -583,35 +552,31 @@ impl<'a> Iterator for BlobDirIter<'a> {
impl FusedIterator for BlobDirIter<'_> {}
fn encode_img(
img: &DynamicImage,
fmt: ImageOutputFormat,
encoded: &mut Vec<u8>,
) -> anyhow::Result<()> {
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, fmt)?;
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
fmt: ImageOutputFormat,
max_bytes: usize,
max_bytes: Option<usize>,
encoded: &mut Vec<u8>,
) -> anyhow::Result<bool> {
encode_img(img, fmt, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
if let Some(max_bytes) = max_bytes {
encode_img(img, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
}
Ok(false)
}
@@ -624,7 +589,7 @@ mod tests {
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::test_utils::{self, TestContext};
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
@@ -849,11 +814,7 @@ mod tests {
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
check_image_size(avatar_src, 1000, 1000);
check_image_size(
&avatar_blob,
constants::BALANCED_AVATAR_SIZE,
constants::BALANCED_AVATAR_SIZE,
);
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
async fn file_size(path_buf: &Path) -> u64 {
let file = File::open(path_buf).await.unwrap();
@@ -861,8 +822,8 @@ mod tests {
}
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
let strict_limits = true;
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
@@ -889,14 +850,10 @@ mod tests {
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(
avatar_cfg,
avatar_src.with_extension("png").to_str().unwrap()
avatar_src.with_extension("jpg").to_str().unwrap()
);
check_image_size(
avatar_cfg,
constants::BALANCED_AVATAR_SIZE,
constants::BALANCED_AVATAR_SIZE,
);
check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -922,29 +879,18 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
send_image_check_mediaquality(
Some("0"),
bytes,
"jpg",
true, // has Exif
1000,
1000,
0,
1000,
1000,
)
.await
.unwrap();
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
"jpg",
true, // has Exif
1000,
1000,
0,
1000,
1000,
WORSE_IMAGE_SIZE,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
@@ -957,87 +903,69 @@ mod tests {
let img_rotated = send_image_check_mediaquality(
Some("0"),
bytes,
"jpg",
true, // has Exif
2000,
1800,
270,
1800,
2000,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let mut buf = Cursor::new(vec![]);
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
img_rotated
.write_to(&mut buf, image::ImageFormat::Jpeg)
.unwrap();
let bytes = buf.into_inner();
// Do this in parallel to speed up the test a bit
// (it still takes very long though)
let bytes2 = bytes.clone();
let join_handle = tokio::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
});
let img_rotated = send_image_check_mediaquality(
Some("1"),
&bytes,
"jpg",
false, // no Exif
1800,
2000,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
1800,
2000,
WORSE_IMAGE_SIZE * 1800 / 2000,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../test-data/image/screenshot.png");
async fn test_recode_image_3() {
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
send_image_check_mediaquality(
Some("0"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
constants::WORSE_IMAGE_SIZE,
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() {
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
send_image_check_mediaquality(
Some("0"),
bytes,
"jpg",
true, // has Exif
1920,
1080,
0,
constants::BALANCED_IMAGE_SIZE,
constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
}
fn assert_correct_rotation(img: &DynamicImage) {
@@ -1057,12 +985,9 @@ mod tests {
assert_eq!(luma, 0);
}
#[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality(
media_quality_config: Option<&str>,
bytes: &[u8],
extension: &str,
has_exif: bool,
original_width: u32,
original_height: u32,
orientation: i32,
@@ -1074,7 +999,7 @@ mod tests {
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
let file = alice.get_blobdir().join("file").with_extension(extension);
let file = alice.get_blobdir().join("file.jpg");
fs::write(&file, &bytes)
.await
@@ -1082,13 +1007,7 @@ mod tests {
check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
let (_, exif) = blob.metadata()?;
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
} else {
assert!(exif.is_none());
}
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
@@ -1109,8 +1028,7 @@ mod tests {
let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?;
let (_, exif) = blob.metadata()?;
assert!(exif.is_none());
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
let img = check_image_size(file, compressed_width, compressed_height);
Ok(img)

View File

@@ -35,9 +35,8 @@ use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
strip_rtlo_characters, time, IsNoneOrEmpty,
create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps,
get_abs_path, gm2local_offset, improve_single_line_input, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
@@ -248,7 +247,7 @@ impl ChatId {
} else {
warn!(
context,
"Cannot create chat, contact {contact_id} does not exist."
"Cannot create chat, contact {} does not exist.", contact_id,
);
bail!("Can not create chat for non-existing contact");
}
@@ -269,26 +268,25 @@ impl ChatId {
create_protected: ProtectionStatus,
param: Option<String>,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
(
paramsv![
chattype,
&grpname,
grpname,
grpid,
create_blocked,
create_smeared_timestamp(context),
create_protected,
param.unwrap_or_default(),
),
],
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
&grpname,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}",
grpname,
grpid,
chat_id,
create_blocked,
@@ -304,7 +302,7 @@ impl ChatId {
"UPDATE contacts
SET selfavatar_sent=?
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
(timestamp, self),
paramsv![timestamp, self],
)
.await?;
Ok(())
@@ -321,7 +319,7 @@ impl ChatId {
.sql
.execute(
"UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
(new_blocked, self),
paramsv![new_blocked, self],
)
.await?;
Ok(count > 0)
@@ -340,14 +338,14 @@ impl ChatId {
if contact_id != ContactId::SELF {
info!(
context,
"Blocking the contact {contact_id} to block 1:1 chat."
"Blocking the contact {} to block 1:1 chat", contact_id
);
Contact::block(context, contact_id).await?;
}
}
}
Chattype::Group => {
info!(context, "Can't block groups yet, deleting the chat.");
info!(context, "Can't block groups yet, deleting the chat");
self.delete(context).await?;
}
Chattype::Mailinglist => {
@@ -435,7 +433,10 @@ impl ChatId {
context
.sql
.execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self))
.execute(
"UPDATE chats SET protected=? WHERE id=?;",
paramsv![protect, self],
)
.await?;
context.emit_event(EventType::ChatModified(self));
@@ -499,7 +500,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
if let Err(e) = self.inner_set_protection(context, protect).await {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
error!(context, "Cannot set protection: {}", e); // make error user-visible
return Err(e);
}
@@ -521,12 +522,12 @@ impl ChatId {
if visibility == ChatVisibility::Archived {
transaction.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
(MessageState::InNoticed, self, MessageState::InFresh),
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)?;
}
transaction.execute(
"UPDATE chats SET archived=? WHERE id=?;",
(visibility, self),
paramsv![visibility, self],
)?;
Ok(())
})
@@ -555,7 +556,7 @@ impl ChatId {
.execute(
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
AND NOT(muted_until=-1 OR muted_until>?)",
(self, time()),
paramsv![self, time()],
)
.await?;
return Ok(());
@@ -573,7 +574,7 @@ impl ChatId {
WHERE state=?
AND hidden=0
AND chat_id=?",
(MessageState::InFresh, self),
paramsv![MessageState::InFresh, self],
)
.await?;
if unread_cnt == 1 {
@@ -584,7 +585,7 @@ impl ChatId {
}
context
.sql
.execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
.await?;
Ok(())
}
@@ -613,23 +614,26 @@ impl ChatId {
.sql
.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
(self,),
paramsv![self],
)
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
.execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self])
.await?;
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
.execute(
"DELETE FROM chats_contacts WHERE chat_id=?;",
paramsv![self],
)
.await?;
context
.sql
.execute("DELETE FROM chats WHERE id=?;", (self,))
.execute("DELETE FROM chats WHERE id=?;", paramsv![self])
.await?;
context.emit_msgs_changed_without_ids();
@@ -685,7 +689,7 @@ impl ChatId {
.sql
.query_get_value(
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
(self, MessageState::OutDraft),
paramsv![self, MessageState::OutDraft],
)
.await?;
Ok(msg_id)
@@ -767,14 +771,14 @@ impl ChatId {
"UPDATE msgs
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
WHERE id=?;",
(
paramsv![
time(),
msg.viewtype,
msg.text.as_deref().unwrap_or(""),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
msg.id
],
)
.await?;
return Ok(true);
@@ -798,7 +802,7 @@ impl ChatId {
hidden,
mime_in_reply_to)
VALUES (?,?,?, ?,?,?,?,?,?);",
(
paramsv![
self,
ContactId::SELF,
time(),
@@ -808,7 +812,7 @@ impl ChatId {
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
),
],
)
.await?;
msg.id = MsgId::new(row_id.try_into()?);
@@ -821,7 +825,7 @@ impl ChatId {
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
(self,),
paramsv![self],
)
.await?;
Ok(count)
@@ -864,7 +868,7 @@ impl ChatId {
WHERE state=?
AND hidden=0
AND chat_id=?;",
(MessageState::InFresh, self),
paramsv![MessageState::InFresh, self],
)
.await?
};
@@ -874,7 +878,7 @@ impl ChatId {
pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
let res: Option<String> = context
.sql
.query_get_value("SELECT param FROM chats WHERE id=?", (self,))
.query_get_value("SELECT param FROM chats WHERE id=?", paramsv![self])
.await?;
Ok(res
.map(|s| s.parse().unwrap_or_default())
@@ -919,13 +923,13 @@ impl ChatId {
let row = sql
.query_row_optional(
&query,
(
paramsv![
self,
MessageState::OutPreparing,
MessageState::OutDraft,
MessageState::OutPending,
MessageState::OutFailed,
),
MessageState::OutFailed
],
f,
)
.await?;
@@ -1047,7 +1051,10 @@ impl ChatId {
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
@@ -1063,14 +1070,14 @@ impl ChatId {
);
info!(
context,
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
"set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
(timestamp, self),
paramsv![timestamp, self],
)
.await?;
@@ -1166,7 +1173,7 @@ impl Chat {
c.blocked, c.locations_send_until, c.muted_until, c.protected
FROM chats c
WHERE c.id=?;",
(chat_id,),
paramsv![chat_id],
|row| {
let c = Chat {
id: chat_id,
@@ -1204,7 +1211,7 @@ impl Chat {
Err(err) => {
error!(
context,
"Failed to load contacts for {}: {:#}.", chat.id, err
"failed to load contacts for {}: {:#}", chat.id, err
);
}
}
@@ -1280,7 +1287,7 @@ impl Chat {
.sql
.execute(
"UPDATE chats SET param=? WHERE id=?",
(self.param.to_string(), self.id),
paramsv![self.param.to_string(), self.id],
)
.await?;
Ok(())
@@ -1462,7 +1469,7 @@ impl Chat {
.sql
.query_get_value(
"SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
(self.id,),
paramsv![self.id],
)
.await?
{
@@ -1543,13 +1550,13 @@ impl Chat {
"INSERT INTO locations \
(timestamp,from_id,chat_id, latitude,longitude,independent)\
VALUES (?,?,?, ?,?,1);",
(
paramsv![
timestamp,
ContactId::SELF,
self.id,
msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
),
],
)
.await
{
@@ -1573,12 +1580,7 @@ impl Chat {
} else {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
};
match html {
Some(html) => Some(tokio::task::block_in_place(move || {
buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
})?),
None => None,
}
html.map(|html| new_html_mimepart(html).build().as_string())
} else {
None
};
@@ -1592,10 +1594,9 @@ impl Chat {
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
state=?, txt=?, subject=?, param=?,
hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
ephemeral_timestamp=?
mime_headers=?, location_id=?, ephemeral_timer=?, ephemeral_timestamp=?
WHERE id=?;",
params_slice![
paramsv![
new_rfc724_mid,
self.id,
ContactId::SELF,
@@ -1639,12 +1640,11 @@ impl Chat {
mime_references,
mime_modified,
mime_headers,
mime_compressed,
location_id,
ephemeral_timer,
ephemeral_timestamp)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
params_slice![
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
paramsv![
new_rfc724_mid,
self.id,
ContactId::SELF,
@@ -1666,7 +1666,6 @@ impl Chat {
],
)
.await?;
context.new_msgs_notify.notify_one();
msg.id = MsgId::new(u32::try_from(raw_id)?);
maybe_set_logging_xdc(context, msg, self.id).await?;
@@ -1856,7 +1855,7 @@ async fn update_special_chat_name(
.sql
.execute(
"UPDATE chats SET name=? WHERE id=? AND name!=?",
(&name, chat_id, &name),
paramsv![name, chat_id, name],
)
.await?;
}
@@ -1919,7 +1918,7 @@ impl ChatIdBlocked {
WHERE c.type=100 -- 100 = Chattype::Single
AND c.id>9 -- 9 = DC_CHAT_ID_LAST_SPECIAL
AND j.contact_id=?;",
(contact_id,),
paramsv![contact_id],
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;
@@ -1970,13 +1969,13 @@ impl ChatIdBlocked {
"INSERT INTO chats
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(
params![
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
create_smeared_timestamp(context),
),
create_smeared_timestamp(context)
],
)?;
let chat_id = ChatId::new(
transaction
@@ -1989,7 +1988,7 @@ impl ChatIdBlocked {
"INSERT INTO chats_contacts
(chat_id, contact_id)
VALUES((SELECT last_insert_rowid()), ?)",
(contact_id,),
params![contact_id],
)?;
Ok(chat_id)
@@ -2026,18 +2025,15 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
// the caller should check if the message text is empty
} else if msg.viewtype.has_file() {
let mut blob = msg
let blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
if msg.viewtype == Viewtype::Image {
if let Err(err) = blob.recode_to_image_size(context).await {
warn!(
context,
"Cannot recode image, using original data: {err:#}."
);
if let Err(e) = blob.recode_to_image_size(context).await {
warn!(context, "Cannot recode image, using original data: {:?}", e);
}
}
msg.param.set(Param::File, blob.as_name());
@@ -2149,7 +2145,7 @@ pub async fn is_contact_in_chat(
.sql
.exists(
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
(chat_id, contact_id),
paramsv![chat_id, contact_id],
)
.await?;
Ok(exists)
@@ -2201,13 +2197,6 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages againts RTLO attacks
if msg.is_system_message() {
if let Some(text) = &msg.text {
msg.text = Some(strip_rtlo_characters(text.as_ref()));
}
}
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
context.emit_msgs_changed(msg.chat_id, msg.id);
@@ -2267,7 +2256,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
warn!(context, "job: cannot get selfavatar-state: {:#}", err);
false
}
};
@@ -2294,7 +2283,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"Message {msg_id} has no recipient, skipping smtp-send."
"message {} has no recipient, skipping smtp-send", msg_id
);
msg_id.set_delivered(context).await?;
return Ok(None);
@@ -2329,27 +2318,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
error!(context, "Failed to set kml sent_timestamp: {:#}", err);
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
{
error!(context, "Failed to set msg_location_id: {err:#}.");
error!(context, "Failed to set msg_location_id: {:#}", err);
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!(context, "Failed to delete sync ids: {err:#}.");
error!(context, "Failed to delete sync ids: {:#}", err);
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
error!(context, "Failed to set selfavatar timestamp: {:#}", err);
}
}
@@ -2370,12 +2359,12 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
.insert(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
(
paramsv![
&rendered_msg.rfc724_mid,
recipients,
&rendered_msg.message,
msg_id,
),
msg_id
],
)
.await?;
Ok(Some(row_id))
@@ -2541,7 +2530,7 @@ pub async fn get_chat_msgs_ex(
OR m.from_id == ?
OR m.to_id == ?
);",
(chat_id, ContactId::INFO, ContactId::INFO),
paramsv![chat_id, ContactId::INFO, ContactId::INFO],
process_row,
process_rows,
)
@@ -2554,7 +2543,7 @@ pub async fn get_chat_msgs_ex(
FROM msgs m
WHERE m.chat_id=?
AND m.hidden=0;",
(chat_id,),
paramsv![chat_id],
process_row,
process_rows,
)
@@ -2572,7 +2561,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
.sql
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
(chat_id,),
paramsv![chat_id],
)
.await?
{
@@ -2622,7 +2611,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
.sql
.exists(
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
(MessageState::InFresh, chat_id),
paramsv![MessageState::InFresh, chat_id],
)
.await?;
if !exists {
@@ -2637,7 +2626,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
WHERE state=?
AND hidden=0
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
)
.await?;
}
@@ -2686,12 +2675,12 @@ pub(crate) async fn mark_old_messages_as_noticed(
AND hidden=0
AND chat_id=?
AND timestamp<=?;",
(
paramsv![
MessageState::InNoticed,
MessageState::InFresh,
msg.chat_id,
msg.sort_timestamp,
),
msg.sort_timestamp
],
)?;
if changed_rows > 0 {
changed_chats.push(msg.chat_id);
@@ -2704,7 +2693,8 @@ pub(crate) async fn mark_old_messages_as_noticed(
if !changed_chats.is_empty() {
info!(
context,
"Marking chats as noticed because there are newer outgoing messages: {changed_chats:?}."
"Marking chats as noticed because there are newer outgoing messages: {:?}",
changed_chats
);
}
@@ -2735,14 +2725,12 @@ pub async fn get_chat_media(
"SELECT id
FROM msgs
WHERE (1=? OR chat_id=?)
AND chat_id != ?
AND (type=? OR type=? OR type=?)
AND hidden=0
ORDER BY timestamp, id;",
(
paramsv![
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
DC_CHAT_ID_TRASH,
msg_type,
if msg_type2 != Viewtype::Unknown {
msg_type2
@@ -2754,7 +2742,7 @@ pub async fn get_chat_media(
} else {
msg_type
},
),
],
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
)
@@ -2832,7 +2820,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
ON c.id=cc.contact_id
WHERE cc.chat_id=?
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
(chat_id,),
paramsv![chat_id],
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
@@ -2858,12 +2846,12 @@ pub async fn create_group_chat(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
paramsv![
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context),
),
],
)
.await?;
@@ -2896,7 +2884,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
.sql
.exists(
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
(Chattype::Broadcast, &better_name),
paramsv![Chattype::Broadcast, better_name],
)
.await?
{
@@ -2916,12 +2904,12 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
paramsv![
Chattype::Broadcast,
chat_name,
grpid,
create_smeared_timestamp(context),
),
],
)
.await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
@@ -2942,7 +2930,7 @@ pub(crate) async fn add_to_chat_contacts_table(
for contact_id in contact_ids {
transaction.execute(
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
(chat_id, contact_id),
paramsv![chat_id, contact_id],
)?;
}
Ok(())
@@ -2962,7 +2950,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
.sql
.execute(
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
(chat_id, contact_id),
paramsv![chat_id, contact_id],
)
.await?;
Ok(())
@@ -3027,7 +3015,7 @@ pub(crate) async fn add_contact_to_chat_ex(
// if SELF is not in the group, members cannot be added at all.
warn!(
context,
"Invalid attempt to add self e-mail address to group."
"invalid attempt to add self e-mail address to group"
);
return Ok(false);
}
@@ -3080,7 +3068,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
FROM chats_contacts cc
LEFT JOIN contacts c ON c.id=cc.contact_id
WHERE cc.chat_id=? AND cc.contact_id!=?;",
(chat_id, ContactId::SELF),
paramsv![chat_id, ContactId::SELF],
|row| Ok(row.get::<_, i64>(0)),
|rows| {
let mut needs_attach = false;
@@ -3153,7 +3141,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
.sql
.execute(
"UPDATE chats SET muted_until=? WHERE id=?;",
(duration, chat_id),
paramsv![duration, chat_id],
)
.await
.context(format!("Failed to set mute duration for {chat_id}"))?;
@@ -3240,7 +3228,10 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
if !is_group_explicitly_left(context, grpid).await? {
context
.sql
.execute("INSERT INTO leftgrps (grpid) VALUES(?);", (grpid,))
.execute(
"INSERT INTO leftgrps (grpid) VALUES(?);",
paramsv![grpid.to_string()],
)
.await?;
}
@@ -3250,7 +3241,10 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result<bool> {
let exists = context
.sql
.exists("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", (grpid,))
.exists(
"SELECT COUNT(*) FROM leftgrps WHERE grpid=?;",
paramsv![grpid],
)
.await?;
Ok(exists)
}
@@ -3282,7 +3276,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
(new_name.to_string(), chat_id),
paramsv![new_name.to_string(), chat_id],
)
.await?;
if chat.is_promoted()
@@ -3535,7 +3529,7 @@ pub(crate) async fn get_chat_id_by_grpid(
.sql
.query_row_optional(
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
(grpid,),
paramsv![grpid],
|row| {
let chat_id = row.get::<_, ChatId>(0)?;
@@ -3568,7 +3562,7 @@ pub async fn add_device_msg_with_importance(
if let Some(label) = label {
if was_device_msg_ever_added(context, label).await? {
info!(context, "Device-message {label} already added.");
info!(context, "device-message {} already added", label);
return Ok(msg_id);
}
}
@@ -3589,7 +3583,7 @@ pub async fn add_device_msg_with_importance(
.sql
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
(chat_id,),
paramsv![chat_id],
)
.await?
{
@@ -3614,7 +3608,7 @@ pub async fn add_device_msg_with_importance(
param,
rfc724_mid)
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
(
paramsv![
chat_id,
ContactId::DEVICE,
ContactId::SELF,
@@ -3626,10 +3620,9 @@ pub async fn add_device_msg_with_importance(
msg.text.as_ref().cloned().unwrap_or_default(),
msg.param.to_string(),
rfc724_mid,
),
],
)
.await?;
context.new_msgs_notify.notify_one();
msg_id = MsgId::new(u32::try_from(row_id)?);
if !msg.hidden {
@@ -3640,7 +3633,10 @@ pub async fn add_device_msg_with_importance(
if let Some(label) = label {
context
.sql
.execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,))
.execute(
"INSERT INTO devmsglabels (label) VALUES (?);",
paramsv![label.to_string()],
)
.await?;
}
@@ -3667,7 +3663,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
.sql
.exists(
"SELECT COUNT(label) FROM devmsglabels WHERE label=?",
(label,),
paramsv![label],
)
.await?;
@@ -3684,7 +3680,10 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
.execute(
"DELETE FROM msgs WHERE from_id=?;",
paramsv![ContactId::DEVICE],
)
.await?;
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
@@ -3727,7 +3726,7 @@ pub(crate) async fn add_info_msg_with_cmd(
context.sql.insert(
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to)
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
(
paramsv![
chat_id,
from_id.unwrap_or(ContactId::INFO),
ContactId::INFO,
@@ -3741,9 +3740,8 @@ pub(crate) async fn add_info_msg_with_cmd(
ephemeral_timer,
param.to_string(),
parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
)
]
).await?;
context.new_msgs_notify.notify_one();
let msg_id = MsgId::new(row_id.try_into()?);
context.emit_msgs_changed(chat_id, msg_id);
@@ -3782,7 +3780,7 @@ pub(crate) async fn update_msg_text_and_timestamp(
.sql
.execute(
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
(text, timestamp, msg_id),
paramsv![text, timestamp, msg_id],
)
.await?;
context.emit_msgs_changed(chat_id, msg_id);
@@ -3795,10 +3793,8 @@ mod tests {
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use tokio::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_info() {
@@ -5147,11 +5143,9 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 1);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
assert_eq!(msg.match_indices("Gr.").count(), 2);
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
assert_eq!(msg.match_indices("Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
@@ -5981,7 +5975,7 @@ mod tests {
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
let second_image_msg_id = send_media(
send_media(
&t,
chat_id2,
Viewtype::Image,
@@ -6083,47 +6077,6 @@ mod tests {
4
);
// Delete an image.
delete_msgs(&t, &[second_image_msg_id]).await?;
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
)
.await?
.len(),
3
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_blob_renaming() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
chat_id,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
let dir = tempfile::tempdir()?;
let file = dir.path().join("harmless_file.\u{202e}txt.exe");
fs::write(&file, "aaa").await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file(file.to_str().unwrap(), None);
let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await;
// the file bob receives should not contain BIDI-control characters
assert_eq!(
Some("$BLOBDIR/harmless_file.txt.exe"),
msg.param.get(Param::File),
);
Ok(())
}
}

View File

@@ -137,7 +137,7 @@ impl Chatlist {
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
process_row,
process_rows,
).await?
@@ -164,7 +164,7 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft,),
paramsv![MessageState::OutDraft],
process_row,
process_rows,
)
@@ -176,7 +176,7 @@ impl Chatlist {
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
if let Err(err) = update_special_chat_names(context).await {
warn!(context, "Cannot update special chat names: {err:#}.")
warn!(context, "cannot update special chat names: {:?}", err)
}
let str_like_cmd = format!("%{query}%");
@@ -198,7 +198,7 @@ impl Chatlist {
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd),
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
)
@@ -228,7 +228,7 @@ impl Chatlist {
AND NOT c.archived=?4
GROUP BY c.id
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
).await?;
@@ -311,17 +311,13 @@ impl Chatlist {
};
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id)
.await
.context("loading message failed")?;
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
if lastmsg.from_id == ContactId::SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::load_from_db(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
@@ -360,31 +356,12 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
.sql
.count(
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
(Blocked::Yes, ChatVisibility::Archived),
paramsv![Blocked::Yes, ChatVisibility::Archived],
)
.await?;
Ok(count)
}
/// Gets the last message of a chat, the message that would also be displayed in the ChatList
/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2`
pub async fn get_last_message_for_chat(
context: &Context,
chat_id: ChatId,
) -> Result<Option<MsgId>> {
context
.sql
.query_get_value(
"SELECT id
FROM msgs
WHERE chat_id=?2
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1",
(MessageState::OutDraft, chat_id),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -308,9 +308,6 @@ pub enum Config {
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
DebugLogging,
/// Last message processed by the bot.
LastMsgId,
}
impl Context {
@@ -361,11 +358,6 @@ impl Context {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
}
/// Returns 32-bit unsigned integer configuration value for the given key.
pub async fn get_config_u32(&self, key: Config) -> Result<u32> {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
}
/// Returns 64-bit signed integer configuration value for the given key.
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
@@ -467,12 +459,6 @@ impl Context {
Ok(())
}
/// Set the given config to an unsigned 32-bit integer value.
pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> {
self.set_config(key, Some(&value.to_string())).await?;
Ok(())
}
/// Set the given config to a boolean value.
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, if value { Some("1") } else { Some("0") })

View File

@@ -1,16 +1,8 @@
//! # Email accounts autoconfiguration process.
//!
//! The module provides automatic lookup of configuration
//! for email providers based on the built-in [provider database],
//! [Mozilla Thunderbird Autoconfiguration protocol]
//! and [Outlook's Autodiscover].
//!
//! [provider database]: crate::provider
//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
//! [Outlook's Autodiscover]: auto_outlook
//! Email accounts autoconfiguration process module.
mod auto_mozilla;
mod auto_outlook;
mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
@@ -164,9 +156,7 @@ async fn on_configure_completed(
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
.log_err(context)
.ok();
.ok_or_log_msg(context, "Cannot add AEAP explanation");
}
}
}

View File

@@ -1,15 +1,15 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: <https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
use std::io::BufRead;
use std::str::FromStr;
use quick_xml::events::{BytesStart, Event};
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
#[derive(Debug)]

View File

@@ -7,9 +7,9 @@ use std::io::BufRead;
use quick_xml::events::Event;
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
/// Result of parsing a single `Protocol` tag.

44
src/configure/read_url.rs Normal file
View File

@@ -0,0 +1,44 @@
use anyhow::{anyhow, format_err};
use crate::context::Context;
use crate::socks::Socks5Config;
pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
match read_url_inner(context, url).await {
Ok(s) => {
info!(context, "Successfully read url {}", url);
Ok(s)
}
Err(e) => {
info!(context, "Can't read URL {}: {:#}", url, e);
Err(format_err!("Can't read URL {}: {:#}", url, e))
}
}
}
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let client = crate::http::get_client(socks5_config)?;
let mut url = url.to_string();
// Follow up to 10 http-redirects
for _i in 0..10 {
let response = client.get(&url).send().await?;
if response.status().is_redirection() {
let headers = response.headers();
let header = headers
.get_all("location")
.iter()
.last()
.ok_or_else(|| anyhow!("Redirection doesn't have a target location"))?
.to_str()?;
info!(context, "Following redirect to {}", header);
url = header.to_string();
continue;
}
return response.text().await.map_err(Into::into);
}
Err(format_err!("Followed 10 redirections"))
}

View File

@@ -192,15 +192,11 @@ pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// How many existing messages shall be fetched after configuration.
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// max. weight of images to send w/o recoding
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
// max. width/height of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
// max. width/height of images scaled down because of being too huge
// max. width/height of images
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;

View File

@@ -32,10 +32,7 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
EmailAddress,
};
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::{chat, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -347,7 +344,7 @@ impl Contact {
c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
(contact_id,),
paramsv![contact_id],
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
@@ -460,7 +457,7 @@ impl Contact {
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
(MessageState::InNoticed, id, MessageState::InFresh),
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
)
.await?;
Ok(())
@@ -493,7 +490,7 @@ impl Contact {
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
paramsv![addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32,],
)
.await?;
Ok(id)
@@ -539,7 +536,7 @@ impl Contact {
return Ok((ContactId::SELF, sth_modified));
}
let mut name = strip_rtlo_characters(name);
let mut name = name;
#[allow(clippy::collapsible_if)]
if origin <= Origin::OutgoingTo {
// The user may accidentally have written to a "noreply" address with another MUA:
@@ -554,7 +551,7 @@ impl Contact {
// For these kind of email addresses, sender and address often don't belong together
// (like hocuri <notifications@github.com>). In this example, hocuri shouldn't
// be saved as the displayname for notifications@github.com.
name = "".to_string();
name = "";
}
}
@@ -608,7 +605,7 @@ impl Contact {
transaction
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
(
paramsv![
new_name,
if update_addr {
addr.to_string()
@@ -626,7 +623,7 @@ impl Contact {
row_authname
},
row_id
),
],
)?;
if update_name || update_authname {
@@ -634,7 +631,7 @@ impl Contact {
// This is one of the few duplicated data, however, getting the chat list is easier this way.
let chat_id: Option<ChatId> = transaction.query_row(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
(Chattype::Single, isize::try_from(row_id)?),
params![Chattype::Single, isize::try_from(row_id)?],
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
@@ -648,7 +645,7 @@ impl Contact {
"SELECT addr, name, authname
FROM contacts
WHERE id=?",
(contact_id,),
params![contact_id],
|row| {
let addr: String = row.get(0)?;
let name: String = row.get(1)?;
@@ -666,7 +663,7 @@ impl Contact {
let count = transaction.execute(
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
(chat_name, chat_id))?;
params![chat_name, chat_id])?;
if count > 0 {
// Chat name updated
@@ -684,7 +681,7 @@ impl Contact {
.execute(
"INSERT INTO contacts (name, addr, origin, authname)
VALUES (?, ?, ?, ?);",
(
params![
if update_name {
name.to_string()
} else {
@@ -697,7 +694,7 @@ impl Contact {
} else {
"".to_string()
}
),
],
)?;
sth_modified = Modifier::Created;
@@ -798,12 +795,12 @@ impl Contact {
ORDER BY c.last_seen DESC, c.id DESC;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 }
if flag_verified_only { 0i32 } else { 1i32 },
])),
|row| row.get::<_, ContactId>(0),
|ids| {
@@ -850,7 +847,7 @@ impl Contact {
ORDER BY last_seen DESC, id DESC;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo
])),
@@ -883,7 +880,7 @@ impl Contact {
.transaction(move |transaction| {
let mut stmt = transaction
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
let rows = stmt.query_map(params![Chattype::Mailinglist, Blocked::Yes], |row| {
let name: String = row.get(0)?;
let grpid: String = row.get(1)?;
Ok((name, grpid))
@@ -905,7 +902,7 @@ impl Contact {
// Always do an update in case the blocking is reset or name is changed.
transaction.execute(
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
(&name, Origin::MailinglistAddress, &grpid),
params![&name, Origin::MailinglistAddress, &grpid],
)?;
}
Ok(())
@@ -920,7 +917,7 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
(ContactId::LAST_SPECIAL,),
paramsv![ContactId::LAST_SPECIAL],
)
.await?;
Ok(count)
@@ -936,7 +933,7 @@ impl Contact {
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL,),
paramsv![ContactId::LAST_SPECIAL],
|row| row.get::<_, ContactId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
@@ -1030,12 +1027,12 @@ impl Contact {
let deleted_contacts = transaction.execute(
"DELETE FROM contacts WHERE id=?
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
(contact_id, contact_id),
paramsv![contact_id, contact_id],
)?;
if deleted_contacts == 0 {
transaction.execute(
"UPDATE contacts SET origin=? WHERE id=?;",
(Origin::Hidden, contact_id),
paramsv![Origin::Hidden, contact_id],
)?;
}
Ok(())
@@ -1063,7 +1060,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
(self.param.to_string(), self.id),
paramsv![self.param.to_string(), self.id],
)
.await?;
Ok(())
@@ -1075,7 +1072,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
(&self.status, self.id),
paramsv![self.status, self.id],
)
.await?;
Ok(())
@@ -1195,7 +1192,9 @@ impl Contact {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
}
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
@@ -1231,7 +1230,7 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
(ContactId::LAST_SPECIAL,),
paramsv![ContactId::LAST_SPECIAL],
)
.await?;
Ok(count)
@@ -1245,7 +1244,10 @@ impl Contact {
let exists = context
.sql
.exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,))
.exists(
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id],
)
.await?;
Ok(exists)
}
@@ -1260,7 +1262,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
(origin, contact_id, origin),
paramsv![origin, contact_id, origin],
)
.await?;
Ok(())
@@ -1289,20 +1291,18 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
strip_rtlo_characters(
&captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str())),
)
captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str()))
} else {
strip_rtlo_characters(name)
name.to_string()
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(strip_rtlo_characters(name), addr.to_string())
(name.to_string(), addr.to_string())
}
}
@@ -1324,7 +1324,7 @@ async fn set_block_contact(
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
(i32::from(new_blocking), contact_id),
paramsv![i32::from(new_blocking), contact_id],
)
.await?;
@@ -1343,7 +1343,7 @@ WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
(new_blocking, Chattype::Single, contact_id),
paramsv![new_blocking, Chattype::Single, contact_id],
)
.await
.is_ok()
@@ -1460,7 +1460,7 @@ pub(crate) async fn update_last_seen(
.sql
.execute(
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
(timestamp, contact_id),
paramsv![timestamp, contact_id],
)
.await?
> 0
@@ -1489,7 +1489,7 @@ pub fn normalize_name(full_name: &str) -> String {
match full_name.as_bytes() {
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
.get(1..full_name.len() - 1)
.map_or("".to_string(), |s| s.trim().to_string()),
.map_or("".to_string(), |s| s.trim().into()),
_ => full_name.to_string(),
}
}
@@ -1575,7 +1575,7 @@ impl RecentlySeenLoop {
.query_map(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
(time() - SEEN_RECENTLY_SECONDS,),
paramsv![time() - SEEN_RECENTLY_SECONDS],
|row| {
let contact_id: ContactId = row.get("id")?;
let last_seen: i64 = row.get("last_seen")?;

View File

@@ -11,13 +11,14 @@ use std::time::{Duration, Instant, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use tokio::sync::{Mutex, RwLock};
use tokio::task;
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::debug_logging::DebugLogging;
use crate::debug_logging::DebugEventLogData;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
@@ -217,11 +218,6 @@ pub struct InnerContext {
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
/// Notify about new messages.
///
/// This causes [`Context::wait_next_msgs`] to wake up.
pub(crate) new_msgs_notify: Notify,
/// Server ID response if ID capability is supported
/// and the server returned non-NIL on the inbox connection.
/// <https://datatracker.ietf.org/doc/html/rfc2971>
@@ -243,10 +239,18 @@ pub struct InnerContext {
pub(crate) last_error: std::sync::RwLock<String>,
/// If debug logging is enabled, this contains all necessary information
///
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
/// because the lock is used from synchronous [`Context::emit_event`].
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
pub(crate) debug_logging: RwLock<Option<DebugLogging>>,
}
#[derive(Debug)]
pub(crate) struct DebugLogging {
/// The message containing the logging xdc
pub(crate) msg_id: MsgId,
/// Handle to the background task responsible for sending
pub(crate) loop_handle: task::JoinHandle<()>,
/// Channel that log events should be send to
/// A background loop will receive and handle them
pub(crate) sender: Sender<DebugEventLogData>,
}
/// The state of ongoing process.
@@ -256,7 +260,7 @@ enum RunningState {
Running { cancel_sender: Sender<()> },
/// Cancel signal has been sent, waiting for ongoing process to be freed.
ShallStop { request: Instant },
ShallStop,
/// There is no ongoing process, a new one can be allocated.
Stopped,
@@ -359,11 +363,6 @@ impl Context {
blobdir.display()
);
let new_msgs_notify = Notify::new();
// Notify once immediately to allow processing old messages
// without starting I/O.
new_msgs_notify.notify_one();
let inner = InnerContext {
id,
blobdir,
@@ -380,12 +379,11 @@ impl Context {
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
debug_logging: RwLock::new(None),
};
let ctx = Context {
@@ -440,18 +438,41 @@ impl Context {
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
if self
.debug_logging
.try_read()
.ok()
.map(|inner| inner.is_some())
== Some(true)
{
let lock = self.debug_logging.read().expect("RwLock is poisoned");
if let Some(debug_logging) = &*lock {
debug_logging.log_event(event.clone());
}
}
self.send_log_event(event.clone()).ok();
};
self.events.emit(Event {
id: self.id,
typ: event,
});
}
pub(crate) fn send_log_event(&self, event: EventType) -> anyhow::Result<()> {
if let Ok(lock) = self.debug_logging.try_read() {
if let Some(DebugLogging {
msg_id: xdc_id,
sender,
..
}) = &*lock
{
let event_data = DebugEventLogData {
time: time(),
msg_id: *xdc_id,
event,
};
sender.try_send(event_data).ok();
}
}
Ok(())
}
/// Emits a generic MsgsChanged event (without chat or message id)
pub fn emit_msgs_changed_without_ids(&self) {
self.emit_event(EventType::MsgsChanged {
@@ -509,9 +530,6 @@ impl Context {
pub(crate) async fn free_ongoing(&self) {
let mut s = self.running_state.write().await;
if let RunningState::ShallStop { request } = *s {
info!(self, "Ongoing stopped in {:?}", request.elapsed());
}
*s = RunningState::Stopped;
}
@@ -524,11 +542,9 @@ impl Context {
warn!(self, "could not cancel ongoing: {:#}", err);
}
info!(self, "Signaling the ongoing process to stop ASAP.",);
*s = RunningState::ShallStop {
request: Instant::now(),
};
*s = RunningState::ShallStop;
}
RunningState::ShallStop { .. } | RunningState::Stopped => {
RunningState::ShallStop | RunningState::Stopped => {
info!(self, "No ongoing process to stop.",);
}
}
@@ -538,7 +554,7 @@ impl Context {
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
match &*self.running_state.read().await {
RunningState::Running { .. } => false,
RunningState::ShallStop { .. } | RunningState::Stopped => true,
RunningState::ShallStop | RunningState::Stopped => true,
}
}
@@ -751,10 +767,6 @@ impl Context {
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
);
res.insert(
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
@@ -787,7 +799,7 @@ impl Context {
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
(MessageState::InFresh, time()),
paramsv![MessageState::InFresh, time()],
|row| row.get::<_, MsgId>(0),
|rows| {
let mut list = Vec::new();
@@ -801,66 +813,6 @@ impl Context {
Ok(list)
}
/// Returns a list of messages with database ID higher than requested.
///
/// Blocked contacts and chats are excluded,
/// but self-sent messages and contact requests are included in the results.
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
None => MsgId::new_unset(),
};
let list = self
.sql
.query_map(
"SELECT m.id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
LEFT JOIN chats c
ON m.chat_id=c.id
WHERE m.id>?
AND m.hidden=0
AND m.chat_id>9
AND ct.blocked=0
AND c.blocked!=1
ORDER BY m.id ASC",
(
last_msg_id.to_u32(), // Explicitly convert to u32 because 0 is allowed.
),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
|rows| {
let mut list = Vec::new();
for row in rows {
list.push(row?);
}
Ok(list)
},
)
.await?;
Ok(list)
}
/// Returns a list of messages with database ID higher than last marked as seen.
///
/// This function is supposed to be used by bot to request messages
/// that are not processed yet.
///
/// Waits for notification and returns a result.
/// Note that the result may be empty if the message is deleted
/// shortly after notification or notification is manually triggered
/// to interrupt waiting.
/// Notification may be manually triggered by calling [`Self::stop_io`].
pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
self.new_msgs_notify.notified().await;
let list = self.get_next_msgs().await?;
Ok(list)
}
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
@@ -872,10 +824,24 @@ impl Context {
}
let str_like_in_text = format!("%{real_query}%");
let do_query = |query, params| {
self.sql.query_map(
query,
params,
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
};
let list = if let Some(chat_id) = chat_id {
self.sql
.query_map(
"SELECT m.id AS id
do_query(
"SELECT m.id AS id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -884,17 +850,9 @@ impl Context {
AND ct.blocked=0
AND txt LIKE ?
ORDER BY m.timestamp,m.id;",
(chat_id, str_like_in_text),
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await?
paramsv![chat_id, str_like_in_text],
)
.await?
} else {
// For performance reasons results are sorted only by `id`, that is in the order of
// message reception.
@@ -906,9 +864,8 @@ impl Context {
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
// The limit is documented and UI may add a hint when getting 1000 results.
self.sql
.query_map(
"SELECT m.id AS id
do_query(
"SELECT m.id AS id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -920,17 +877,9 @@ impl Context {
AND ct.blocked=0
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
(str_like_in_text,),
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await?
paramsv![str_like_in_text],
)
.await?
};
Ok(list)
@@ -1140,7 +1089,7 @@ mod tests {
t.sql
.execute(
"UPDATE chats SET muted_until=? WHERE id=?;",
(time() - 3600, bob.id),
paramsv![time() - 3600, bob.id],
)
.await
.unwrap();
@@ -1157,7 +1106,10 @@ mod tests {
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
// that results in "muted forever" by definition.
t.sql
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
.execute(
"UPDATE chats SET muted_until=-2 WHERE id=?;",
paramsv![bob.id],
)
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
@@ -1492,38 +1444,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_next_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
assert!(alice.get_next_msgs().await?.is_empty());
assert!(bob.get_next_msgs().await?.is_empty());
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
let received_msg = bob.recv_msg(&sent_msg).await;
let bob_next_msg_ids = bob.get_next_msgs().await?;
assert_eq!(bob_next_msg_ids.len(), 1);
assert_eq!(bob_next_msg_ids.get(0), Some(&received_msg.id));
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
.await?;
assert!(bob.get_next_msgs().await?.is_empty());
// Next messages include self-sent messages.
let alice_next_msg_ids = alice.get_next_msgs().await?;
assert_eq!(alice_next_msg_ids.len(), 1);
assert_eq!(alice_next_msg_ids.get(0), Some(&sent_msg.sender_msg_id));
alice
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
.await?;
assert!(alice.get_next_msgs().await?.is_empty());
Ok(())
}
}

View File

@@ -2,41 +2,17 @@
use crate::{
chat::ChatId,
config::Config,
context::Context,
context::{Context, DebugLogging},
message::{Message, MsgId, Viewtype},
param::Param,
tools::time,
webxdc::StatusUpdateItem,
EventType,
Event, EventType,
};
use async_channel::{self as channel, Receiver, Sender};
use async_channel::{self as channel, Receiver};
use serde_json::json;
use std::path::PathBuf;
use tokio::task;
#[derive(Debug)]
pub(crate) struct DebugLogging {
/// The message containing the logging xdc
pub(crate) msg_id: MsgId,
/// Handle to the background task responsible for sending
pub(crate) loop_handle: task::JoinHandle<()>,
/// Channel that log events should be sent to.
/// A background loop will receive and handle them.
pub(crate) sender: Sender<DebugEventLogData>,
}
impl DebugLogging {
pub(crate) fn log_event(&self, event: EventType) {
let event_data = DebugEventLogData {
time: time(),
msg_id: self.msg_id,
event,
};
self.sender.try_send(event_data).ok();
}
}
/// Store all information needed to log an event to a webxdc.
pub struct DebugEventLogData {
pub time: i64,
@@ -72,9 +48,12 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
eprintln!("Can't log event to webxdc status update: {err:#}");
}
Ok(serial) => {
context.emit_event(EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: serial,
context.events.emit(Event {
id: context.id,
typ: EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: serial,
},
});
}
}
@@ -133,26 +112,24 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option<MsgId>) -> a
Some(msg_id.to_string().as_ref()),
)
.await?;
{
let debug_logging = &mut *ctx.debug_logging.write().expect("RwLock is poisoned");
match debug_logging {
// Switch logging xdc
Some(debug_logging) => debug_logging.msg_id = msg_id,
// Bootstrap background loop for message forwarding
None => {
let (sender, debug_logging_recv) = channel::bounded(1000);
let loop_handle = {
let ctx = ctx.clone();
task::spawn(async move {
debug_logging_loop(&ctx, debug_logging_recv).await
})
};
*debug_logging = Some(DebugLogging {
msg_id,
loop_handle,
sender,
});
}
let debug_logging = &mut *ctx.debug_logging.write().await;
match debug_logging {
// Switch logging xdc
Some(debug_logging) => debug_logging.msg_id = msg_id,
// Bootstrap background loop for message forwarding
None => {
let (sender, debug_logging_recv) = channel::bounded(1000);
let loop_handle = {
let ctx = ctx.clone();
task::spawn(
async move { debug_logging_loop(&ctx, debug_logging_recv).await },
)
};
*debug_logging = Some(DebugLogging {
msg_id,
loop_handle,
sender,
});
}
}
info!(ctx, "replacing logging webxdc");
@@ -162,7 +139,7 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option<MsgId>) -> a
ctx.sql
.set_raw_config(Config::DebugLogging.as_ref(), None)
.await?;
*ctx.debug_logging.write().expect("RwLock is poisoned") = None;
*ctx.debug_logging.write().await = None;
info!(ctx, "removing logging webxdc");
}
}

View File

@@ -101,7 +101,7 @@ impl MsgId {
.sql
.execute(
"UPDATE msgs SET download_state=? WHERE id=?;",
(download_state, self),
paramsv![download_state, self],
)
.await?;
context.emit_event(EventType::MsgsChanged {
@@ -134,7 +134,7 @@ impl Job {
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
(&msg.rfc724_mid,),
paramsv![msg.rfc724_mid],
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;

View File

@@ -174,7 +174,10 @@ impl ChatId {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
let timer = context
.sql
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,))
.query_get_value(
"SELECT ephemeral_timer FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(timer.unwrap_or_default())
}
@@ -196,7 +199,7 @@ impl ChatId {
"UPDATE chats
SET ephemeral_timer=?
WHERE id=?;",
(timer, self),
paramsv![timer, self],
)
.await?;
@@ -288,7 +291,10 @@ impl MsgId {
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
let res = match context
.sql
.query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,))
.query_get_value(
"SELECT ephemeral_timer FROM msgs WHERE id=?",
paramsv![self],
)
.await?
{
None | Some(0) => Timer::Disabled,
@@ -308,7 +314,7 @@ impl MsgId {
"UPDATE msgs SET ephemeral_timestamp = ? \
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
AND id = ?",
(ephemeral_timestamp, ephemeral_timestamp, self),
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
)
.await?;
context.scheduler.interrupt_ephemeral_task().await;
@@ -332,8 +338,8 @@ pub(crate) async fn start_ephemeral_timers_msgids(
sql::repeat_vars(msg_ids.len())
),
rusqlite::params_from_iter(
std::iter::once(&now as &dyn crate::sql::ToSql)
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
std::iter::once(&now as &dyn crate::ToSql)
.chain(std::iter::once(&now as &dyn crate::ToSql))
.chain(params_iter(msg_ids)),
),
)
@@ -363,7 +369,7 @@ WHERE
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
(now, DC_CHAT_ID_TRASH),
paramsv![now, DC_CHAT_ID_TRASH],
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
@@ -396,12 +402,12 @@ WHERE
AND chat_id != ?
AND chat_id != ?
"#,
(
paramsv![
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id,
),
device_chat_id
],
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
@@ -443,7 +449,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
SET chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE id=?",
(DC_CHAT_ID_TRASH, msg_id),
params![DC_CHAT_ID_TRASH, msg_id],
)?;
msgs_changed.push((chat_id, msg_id));
@@ -488,7 +494,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
AND chat_id != ?
AND chat_id != ?;
"#,
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
paramsv![DC_CHAT_ID_TRASH, self_chat_id, device_chat_id],
)
.await?;
@@ -512,7 +518,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
WHERE ephemeral_timestamp != 0
AND chat_id != ?;
"#,
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
)
.await
{
@@ -569,8 +575,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
delete_expired_messages(context, time())
.await
.log_err(context)
.ok();
.ok_or_log(context);
}
}
@@ -599,12 +604,12 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)",
(
&target,
paramsv![
target,
threshold_timestamp,
threshold_timestamp_extended,
now,
),
],
)
.await?;
@@ -629,12 +634,12 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
WHERE ephemeral_timer > 0 \
AND ephemeral_timestamp = 0 \
AND state NOT IN (?, ?, ?)",
(
paramsv![
time(),
MessageState::InFresh,
MessageState::InNoticed,
MessageState::OutDraft,
),
MessageState::OutDraft
],
)
.await?;
@@ -1100,7 +1105,7 @@ mod tests {
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
.await
.unwrap();
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
@@ -1125,13 +1130,13 @@ mod tests {
t.sql
.execute(
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
(id, &message_id, timestamp, ephemeral_timestamp),
paramsv![id, message_id, timestamp, ephemeral_timestamp],
)
.await?;
t.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
(&message_id, id),
paramsv![message_id, id],
)
.await?;
}
@@ -1142,7 +1147,7 @@ mod tests {
.sql
.count(
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
(id.to_string(),),
paramsv![id.to_string()],
)
.await?,
1
@@ -1153,7 +1158,10 @@ mod tests {
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
context
.sql
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
.execute(
"DELETE FROM imap WHERE rfc724_mid=?",
paramsv![id.to_string()],
)
.await?;
Ok(())
}

View File

@@ -1,10 +1,15 @@
//! # Events specification.
use std::path::PathBuf;
use async_channel::{self as channel, Receiver, Sender, TrySendError};
use serde::Serialize;
mod payload;
pub use self::payload::EventType;
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
/// Event channel.
#[derive(Debug, Clone)]
@@ -27,9 +32,7 @@ impl Events {
Self { receiver, sender }
}
/// Emits an event into event channel.
///
/// If the channel is full, deletes the oldest event first.
/// Emits an event.
pub fn emit(&self, event: Event) {
match self.sender.try_send(event) {
Ok(()) => {}
@@ -46,7 +49,7 @@ impl Events {
}
}
/// Creates an event emitter.
/// Retrieve the event emitter.
pub fn get_emitter(&self) -> EventEmitter {
EventEmitter(self.receiver.clone())
}
@@ -105,3 +108,248 @@ pub struct Event {
/// These are documented in `deltachat.h` as the `DC_EVENT_*` constants.
pub typ: EventType,
}
/// Event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum EventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info(String),
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent(String),
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
ImapMessageMoved(String),
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile(String),
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning(String),
/// The library-user should report an error to the end-user.
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error(String),
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
ErrorSelfNotInGroup(String),
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
MsgsChanged {
/// Set if only a single chat is affected by the changes, otherwise 0.
chat_id: ChatId,
/// Set if only a single message is affected by the changes, otherwise 0.
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
IncomingMsg {
/// ID of the chat where the message is assigned.
chat_id: ChatId,
/// ID of the message.
msg_id: MsgId,
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
MsgDelivered {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was successfully sent.
msg_id: MsgId,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
MsgFailed {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that could not be sent.
msg_id: MsgId,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
MsgRead {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was read.
msg_id: MsgId,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
ChatModified(ChatId),
/// Chat ephemeral timer changed.
ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
ContactsChanged(Option<ContactId>),
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
ImexProgress(usize),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
ImexFileWritten(PathBuf),
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr().
SecurejoinInviterProgress {
/// ID of the contact that wants to join.
contact_id: ContactId,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed.
SecurejoinJoinerProgress {
/// ID of the inviting contact.
contact_id: ContactId,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
/// dc_get_connectivity_html() for details.
ConnectivityChanged,
/// The user's avatar changed.
SelfavatarChanged,
/// Webxdc status update received.
WebxdcStatusUpdate {
/// Message ID.
msg_id: MsgId,
/// Status update ID.
status_update_serial: StatusUpdateSerial,
},
/// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted {
/// ID of the deleted message.
msg_id: MsgId,
},
}

View File

@@ -1,258 +0,0 @@
//! # Event payloads.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
/// Event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info(String),
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent(String),
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
ImapMessageMoved(String),
/// Emitted before going into IDLE on the Inbox folder.
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile(String),
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning(String),
/// The library-user should report an error to the end-user.
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error(String),
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
ErrorSelfNotInGroup(String),
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
MsgsChanged {
/// Set if only a single chat is affected by the changes, otherwise 0.
chat_id: ChatId,
/// Set if only a single message is affected by the changes, otherwise 0.
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
IncomingMsg {
/// ID of the chat where the message is assigned.
chat_id: ChatId,
/// ID of the message.
msg_id: MsgId,
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
MsgDelivered {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was successfully sent.
msg_id: MsgId,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
MsgFailed {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that could not be sent.
msg_id: MsgId,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
MsgRead {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was read.
msg_id: MsgId,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
ChatModified(ChatId),
/// Chat ephemeral timer changed.
ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
ContactsChanged(Option<ContactId>),
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
ImexProgress(usize),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
ImexFileWritten(PathBuf),
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr().
SecurejoinInviterProgress {
/// ID of the contact that wants to join.
contact_id: ContactId,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed.
SecurejoinJoinerProgress {
/// ID of the inviting contact.
contact_id: ContactId,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
/// dc_get_connectivity_html() for details.
ConnectivityChanged,
/// The user's avatar changed.
SelfavatarChanged,
/// Webxdc status update received.
WebxdcStatusUpdate {
/// Message ID.
msg_id: MsgId,
/// Status update ID.
status_update_serial: StatusUpdateSerial,
},
/// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted {
/// ID of the deleted message.
msg_id: MsgId,
},
}

24
src/http.rs Normal file
View File

@@ -0,0 +1,24 @@
//! # HTTP module.
use std::time::Duration;
use anyhow::Result;
use crate::socks::Socks5Config;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)
} else {
// Disable usage of "system" proxy configured via environment variables.
// It is enabled by default in `reqwest`, see
// <https://docs.rs/reqwest/0.11.14/reqwest/struct.ClientBuilder.html#method.no_proxy>
// for documentation.
builder.no_proxy()
};
Ok(builder.build()?)
}

View File

@@ -14,7 +14,7 @@ use std::{
use anyhow::{bail, format_err, Context as _, Result};
use async_channel::Receiver;
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use futures::{StreamExt, TryStreamExt};
use futures::StreamExt;
use num_traits::FromPrimitive;
use crate::chat::{self, ChatId, ChatIdBlocked};
@@ -516,7 +516,8 @@ impl Imap {
.uid_fetch("1:*", RFC724MID_UID)
.await
.with_context(|| format!("can't resync folder {folder}"))?;
while let Some(fetch) = list.try_next().await? {
while let Some(fetch) = list.next().await {
let fetch = fetch?;
let headers = match get_fetch_headers(&fetch) {
Ok(headers) => headers,
Err(err) => {
@@ -550,7 +551,7 @@ impl Imap {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?;
for (uid, (rfc724_mid, target)) in &msgs {
// This may detect previously undetected moved
// messages, so we update server_folder too.
@@ -560,7 +561,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(rfc724_mid, folder, uid, uid_validity, target),
params![rfc724_mid, folder, uid, uid_validity, target],
)?;
}
Ok(())
@@ -659,7 +660,7 @@ impl Imap {
.context("Error fetching UID")?;
let mut new_last_seen_uid = None;
while let Some(fetch) = list.try_next().await? {
while let Some(fetch) = list.next().await.transpose()? {
if fetch.message == mailbox.exists && fetch.uid.is_some() {
new_last_seen_uid = fetch.uid;
}
@@ -676,7 +677,7 @@ impl Imap {
.sql
.execute(
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
(&folder, new_uid_validity),
paramsv![folder, new_uid_validity],
)
.await?;
@@ -759,7 +760,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(&message_id, &folder, uid, uid_validity, &target),
paramsv![message_id, folder, uid, uid_validity, &target],
)
.await?;
@@ -1050,7 +1051,7 @@ impl Session {
WHERE folder = ?
AND target != folder
ORDER BY target, uid",
(folder,),
paramsv![folder],
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
@@ -1197,11 +1198,8 @@ impl Imap {
.await
.context("failed to fetch flags")?;
while let Some(fetch) = list
.try_next()
.await
.context("failed to get FETCH result")?
{
while let Some(fetch) = list.next().await {
let fetch = fetch.context("failed to get FETCH result")?;
let uid = if let Some(uid) = fetch.uid {
uid
} else {
@@ -1260,7 +1258,8 @@ impl Imap {
.await
.context("IMAP Could not fetch")?;
while let Some(msg) = list.try_next().await? {
while let Some(fetch) = list.next().await {
let msg = fetch?;
match get_fetch_headers(&msg) {
Ok(headers) => {
if let Some(from) = mimeparser::get_from(&headers) {
@@ -1295,7 +1294,8 @@ impl Imap {
.context("IMAP could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
while let Some(fetch) = list.next().await {
let msg = fetch?;
if let Some(msg_uid) = msg.uid {
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
@@ -1324,15 +1324,16 @@ impl Imap {
// Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages.
// Sequence numbers are sequential. If there are 1000 messages in the inbox,
// we can fetch the sequence numbers 900-1000 and get the last 100 messages.
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT + 1);
let set = format!("{first}:{exists}");
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT);
let set = format!("{first}:*");
let mut list = session
.fetch(&set, PREFETCH_FLAGS)
.await
.context("IMAP Could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
while let Some(fetch) = list.next().await {
let msg = fetch?;
if let Some(msg_uid) = msg.uid {
msgs.insert((msg.internal_date(), msg_uid), msg);
}
@@ -1679,7 +1680,8 @@ impl Imap {
let mut delimiter_is_default = true;
let mut folder_configs = BTreeMap::new();
while let Some(folder) = folders.try_next().await? {
while let Some(folder) = folders.next().await {
let folder = folder?;
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
@@ -1740,37 +1742,17 @@ impl Session {
/// If this returns `true`, this means that new emails arrived and you should
/// fetch again, even if you just fetched.
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
use UnsolicitedResponse::*;
let mut unsolicited_exists = false;
while let Ok(response) = self.unsolicited_responses.try_recv() {
match response {
Exists(_) => {
UnsolicitedResponse::Exists(_) => {
info!(
context,
"Need to fetch again, got unsolicited EXISTS {:?}", response
);
unsolicited_exists = true;
}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them
Expunge(_) | Recent(_) => {}
Other(response_data)
if matches!(
response_data.parsed(),
Response::Fetch { .. }
| Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
}
) => {}
_ => {
info!(context, "got unsolicited response {:?}", response)
}
_ => info!(context, "ignoring unsolicited response {:?}", response),
}
}
Ok(unsolicited_exists)
@@ -2188,7 +2170,7 @@ async fn mark_seen_by_uid(
AND uid=?3
LIMIT 1
)",
(&folder, uid_validity, uid),
paramsv![&folder, uid_validity, uid],
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;
@@ -2204,12 +2186,12 @@ async fn mark_seen_by_uid(
"UPDATE msgs SET state=?1
WHERE (state=?2 OR state=?3)
AND id=?4",
(
paramsv![
MessageState::InSeen,
MessageState::InFresh,
MessageState::InNoticed,
msg_id,
),
msg_id
],
)
.await
.with_context(|| format!("failed to update msg {msg_id} state"))?
@@ -2239,7 +2221,7 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
.execute(
"INSERT OR IGNORE INTO imap_markseen (id)
SELECT id FROM imap WHERE rfc724_mid=?",
(message_id,),
paramsv![message_id],
)
.await?;
context
@@ -2259,7 +2241,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
(folder, uid_next),
paramsv![folder, uid_next],
)
.await?;
Ok(())
@@ -2273,7 +2255,10 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
.query_get_value(
"SELECT uid_next FROM imap_sync WHERE folder=?;",
paramsv![folder],
)
.await?
.unwrap_or(0))
}
@@ -2288,7 +2273,7 @@ pub(crate) async fn set_uidvalidity(
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
(folder, uidvalidity),
paramsv![folder, uidvalidity],
)
.await?;
Ok(())
@@ -2299,7 +2284,7 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
.sql
.query_get_value(
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
(folder,),
paramsv![folder],
)
.await?
.unwrap_or(0))
@@ -2311,7 +2296,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
(folder, modseq),
paramsv![folder, modseq],
)
.await?;
Ok(())
@@ -2320,7 +2305,10 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
Ok(context
.sql
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
.query_get_value(
"SELECT modseq FROM imap_sync WHERE folder=?;",
paramsv![folder],
)
.await?
.unwrap_or(0))
}

View File

@@ -70,9 +70,7 @@ impl Imap {
loop {
self.fetch_move_delete(context, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
@@ -107,11 +105,7 @@ impl Imap {
let list = session
.list(Some(""), Some("*"))
.await?
.filter_map(|f| async {
f.context("list_folders() can't get folder")
.log_err(context)
.ok()
});
.filter_map(|f| async { f.ok_or_log_msg(context, "list_folders() can't get folder") });
Ok(list.collect().await)
}
}

View File

@@ -91,13 +91,15 @@ pub async fn imex(
let cancel = context.alloc_ongoing().await?;
let res = {
let _guard = context.scheduler.pause(context.clone()).await?;
imex_inner(context, what, path, passphrase)
let mut guard = context.scheduler.pause(context.clone()).await;
let res = imex_inner(context, what, path, passphrase)
.race(async {
cancel.recv().await.ok();
Err(format_err!("canceled"))
})
.await
.await;
guard.resume().await;
res
};
context.free_ongoing().await;
@@ -397,7 +399,8 @@ async fn imex_inner(
export_backup(context, path, passphrase.unwrap_or_default()).await
}
ImexMode::ImportBackup => {
import_backup(context, path, passphrase.unwrap_or_default()).await
import_backup(context, path, passphrase.unwrap_or_default()).await?;
context.sql.run_migrations(context).await
}
}
}
@@ -473,7 +476,6 @@ async fn import_backup(
}
}
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
@@ -759,15 +761,18 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
context.sql.set_raw_config_int("backup_time", now).await?;
sql::housekeeping(context).await.log_err(context).ok();
sql::housekeeping(context).await.ok_or_log(context);
context
.sql
.call_write(|conn| {
conn.execute("VACUUM;", ())
conn.execute("VACUUM;", params![])
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
.ok();
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
.context("failed to attach backup database")?;
conn.execute(
"ATTACH DATABASE ? AS backup KEY ?",
paramsv![dest, passphrase],
)
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
.context("failed to export to attached backup database");

View File

@@ -32,8 +32,7 @@ use std::task::Poll;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use async_channel::Receiver;
use futures_lite::StreamExt;
use iroh::blobs::Collection;
use iroh::get::DataStream;
use iroh::get::{DataStream, Options};
use iroh::progress::ProgressEmitter;
use iroh::protocol::AuthToken;
use iroh::provider::{DataSource, Event, Provider, Ticket};
@@ -44,20 +43,15 @@ use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, Mutex};
use tokio::task::{JoinHandle, JoinSet};
use tokio_stream::wrappers::ReadDirStream;
use tokio_util::sync::CancellationToken;
use crate::blob::BlobDirContents;
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
use crate::chat::delete_and_reset_all_device_msgs;
use crate::context::Context;
use crate::message::{Message, Viewtype};
use crate::qr::{self, Qr};
use crate::stock_str::backup_transfer_msg_body;
use crate::qr::Qr;
use crate::{e2ee, EventType};
use super::{export_database, DBFILE_BACKUP_NAME};
const MAX_CONCURRENT_DIALS: u8 = 16;
/// Provide or send a backup of this device.
///
/// This creates a backup of the current device and starts a service which offers another
@@ -77,8 +71,6 @@ pub struct BackupProvider {
handle: JoinHandle<Result<()>>,
/// The ticket to retrieve the backup collection.
ticket: Ticket,
/// Guard to cancel the provider on drop.
_drop_guard: tokio_util::sync::DropGuard,
}
impl BackupProvider {
@@ -99,11 +91,11 @@ impl BackupProvider {
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let paused_guard = context.scheduler.pause(context.clone()).await?;
let mut paused_guard = context.scheduler.pause(context.clone()).await;
let context_dir = context
.get_blobdir()
.parent()
.ok_or_else(|| anyhow!("Context dir not found"))?;
.ok_or(anyhow!("Context dir not found"))?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
@@ -127,28 +119,21 @@ impl BackupProvider {
Ok((provider, ticket)) => (provider, ticket),
Err(err) => {
context.free_ongoing().await;
paused_guard.resume().await;
return Err(err);
}
};
let drop_token = CancellationToken::new();
let handle = {
let context = context.clone();
let drop_token = drop_token.clone();
tokio::spawn(async move {
let res = Self::watch_provider(&context, provider, cancel_token, drop_token).await;
let res = Self::watch_provider(&context, provider, cancel_token).await;
context.free_ongoing().await;
// Explicit drop to move the guards into this future
drop(paused_guard);
paused_guard.resume().await;
drop(dbfile);
res
})
};
Ok(Self {
handle,
ticket,
_drop_guard: drop_token.drop_guard(),
})
Ok(Self { handle, ticket })
}
/// Creates the provider task.
@@ -184,7 +169,7 @@ impl BackupProvider {
.spawn()?;
context.emit_event(SendProgress::ProviderListening.into());
info!(context, "Waiting for remote to connect");
let ticket = provider.ticket(hash)?;
let ticket = provider.ticket(hash);
Ok((provider, ticket))
}
@@ -202,8 +187,8 @@ impl BackupProvider {
context: &Context,
mut provider: Provider,
cancel_token: Receiver<()>,
drop_token: CancellationToken,
) -> Result<()> {
// _dbfile exists so we can clean up the file once it is no longer needed
let mut events = provider.subscribe();
let mut total_size = 0;
let mut current_size = 0;
@@ -263,21 +248,12 @@ impl BackupProvider {
},
_ = cancel_token.recv() => {
provider.shutdown();
break Err(anyhow!("BackupProvider cancelled"));
break Err(anyhow!("BackupSender cancelled"));
},
_ = drop_token.cancelled() => {
provider.shutdown();
break Err(anyhow!("BackupProvider dropped"));
}
}
};
match &res {
Ok(_) => {
context.emit_event(SendProgress::Completed.into());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(backup_transfer_msg_body(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
}
Ok(_) => context.emit_event(SendProgress::Completed.into()),
Err(err) => {
error!(context, "Backup transfer failure: {err:#}");
context.emit_event(SendProgress::Failed.into())
@@ -393,20 +369,19 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
let mut guard = context.scheduler.pause(context.clone()).await;
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let _guard = context.scheduler.pause(context.clone()).await;
info!(
context,
"Running get_backup for {}",
qr::format_backup(&qr)?
);
let res = tokio::select! {
biased;
res = get_backup_inner(context, qr) => res,
res = get_backup_inner(context, qr) => {
context.free_ongoing().await;
res
}
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
context.free_ongoing().await;
guard.resume().await;
res
}
@@ -415,68 +390,104 @@ async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
Qr::Backup { ticket } => ticket,
_ => bail!("QR code for backup must be of type DCBACKUP"),
};
match transfer_from_provider(context, &ticket).await {
Ok(()) => {
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
context.emit_event(ReceiveProgress::Completed.into());
Ok(())
}
Err(err) => {
// Clean up any blobs we already wrote.
let readdir = fs::read_dir(context.get_blobdir()).await?;
let mut readdir = ReadDirStream::new(readdir);
while let Some(dirent) = readdir.next().await {
if let Ok(dirent) = dirent {
fs::remove_file(dirent.path()).await.ok();
}
if ticket.addrs.is_empty() {
bail!("ticket is missing addresses to dial");
}
for addr in &ticket.addrs {
let opts = Options {
addr: *addr,
peer_id: Some(ticket.peer),
keylog: false,
};
info!(context, "attempting to contact {}", addr);
match transfer_from_provider(context, &ticket, opts).await {
Ok(_) => {
delete_and_reset_all_device_msgs(context).await?;
context.emit_event(ReceiveProgress::Completed.into());
return Ok(());
}
Err(TransferError::ConnectionError(err)) => {
warn!(context, "Connection error: {err:#}.");
continue;
}
Err(TransferError::Other(err)) => {
// Clean up any blobs we already wrote.
let readdir = fs::read_dir(context.get_blobdir()).await?;
let mut readdir = ReadDirStream::new(readdir);
while let Some(dirent) = readdir.next().await {
if let Ok(dirent) = dirent {
fs::remove_file(dirent.path()).await.ok();
}
}
context.emit_event(ReceiveProgress::Failed.into());
return Err(err);
}
context.emit_event(ReceiveProgress::Failed.into());
Err(err)
}
}
Err(anyhow!("failed to contact provider"))
}
async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()> {
/// Error during a single transfer attempt.
///
/// Mostly exists to distinguish between `ConnectionError` and any other errors.
#[derive(Debug, thiserror::Error)]
enum TransferError {
#[error("connection error")]
ConnectionError(#[source] anyhow::Error),
#[error("other")]
Other(#[source] anyhow::Error),
}
async fn transfer_from_provider(
context: &Context,
ticket: &Ticket,
opts: Options,
) -> Result<(), TransferError> {
let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress());
spawn_progress_proxy(context.clone(), progress.subscribe());
let mut connected = false;
let on_connected = || {
context.emit_event(ReceiveProgress::Connected.into());
async { Ok(()) }
};
let on_collection = |collection: &Collection| {
context.emit_event(ReceiveProgress::CollectionReceived.into());
progress.set_total(collection.total_blobs_size());
connected = true;
async { Ok(()) }
};
let jobs = Mutex::new(JoinSet::default());
let on_blob =
|hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name);
// Perform the transfer.
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
let stats = iroh::get::run_ticket(
ticket,
keylog,
MAX_CONCURRENT_DIALS,
let res = iroh::get::run(
ticket.hash,
ticket.token,
opts,
on_connected,
on_collection,
|collection| {
context.emit_event(ReceiveProgress::CollectionReceived.into());
progress.set_total(collection.total_blobs_size());
async { Ok(()) }
},
on_blob,
)
.await?;
.await;
let mut jobs = jobs.lock().await;
while let Some(job) = jobs.join_next().await {
job.context("job failed")?;
job.context("job failed").map_err(TransferError::Other)?;
}
drop(progress);
info!(
context,
"Backup transfer finished, transfer rate was {} Mbps.",
stats.mbits()
);
Ok(())
match res {
Ok(stats) => {
info!(
context,
"Backup transfer finished, transfer rate is {} Mbps.",
stats.mbits()
);
Ok(())
}
Err(err) => match connected {
true => Err(TransferError::Other(err)),
false => Err(TransferError::ConnectionError(err)),
},
}
}
/// Get callback when a blob is received from the provider.
@@ -497,7 +508,7 @@ async fn on_blob(
let context_dir = context
.get_blobdir()
.parent()
.ok_or_else(|| anyhow!("Context dir not found"))?;
.ok_or(anyhow!("Context dir not found"))?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
@@ -518,7 +529,7 @@ async fn on_blob(
if name.starts_with("db/") {
let context = context.clone();
let token = ticket.token().to_string();
let token = ticket.token.to_string();
jobs.lock().await.spawn(async move {
if let Err(err) = context.sql.import(&path, token).await {
error!(context, "cannot import database: {:#?}", err);
@@ -680,16 +691,4 @@ mod tests {
assert_eq!(out, EventType::ImexProgress(progress));
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_drop_provider() {
let mut tcm = TestContextManager::new();
let ctx = tcm.alice().await;
let provider = BackupProvider::prepare(&ctx).await.unwrap();
drop(provider);
ctx.evtracker
.get_matching(|ev| matches!(ev, EventType::ImexProgress(0)))
.await;
}
}

View File

@@ -99,7 +99,7 @@ impl Job {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,))
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
.await?;
}
@@ -117,22 +117,22 @@ impl Job {
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
(
paramsv![
self.desired_timestamp,
i64::from(self.tries),
self.job_id as i32,
),
],
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
(
paramsv![
self.added_timestamp,
self.action,
self.foreign_id,
self.desired_timestamp
)
]
).await?;
}
@@ -153,7 +153,7 @@ impl<'a> Connection<'a> {
}
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
info!(context, "Job {} started...", &job);
info!(context, "job {} started...", &job);
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
@@ -165,7 +165,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
let tries = job.tries + 1;
if tries < JOB_RETRIES {
info!(context, "Increase job {job} tries to {tries}.");
info!(context, "increase job {} tries to {}", job, tries);
job.tries = tries;
let time_offset = get_backoff_time_offset(tries);
job.desired_timestamp = time() + time_offset;
@@ -177,23 +177,26 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
time_offset
);
job.save(context).await.unwrap_or_else(|err| {
error!(context, "Failed to save job: {err:#}.");
error!(context, "failed to save job: {:#}", err);
});
} else {
info!(
context,
"Remove job {job} as it exhausted {JOB_RETRIES} retries."
"remove job {} as it exhausted {} retries", job, JOB_RETRIES
);
job.delete(context).await.unwrap_or_else(|err| {
error!(context, "Failed to delete job: {err:#}.");
error!(context, "failed to delete job: {:#}", err);
});
}
}
Status::Finished(res) => {
if let Err(err) = res {
warn!(context, "Remove job {job} as it failed with error {err:#}.");
warn!(
context,
"remove job {} as it failed with error {:#}", job, err
);
} else {
info!(context, "Remove job {job} as it succeeded.");
info!(context, "remove job {} as it succeeded", job);
}
job.delete(context).await.unwrap_or_else(|err| {
@@ -209,13 +212,13 @@ async fn perform_job_action(
connection: &mut Connection<'_>,
tries: u32,
) -> Status {
info!(context, "Begin immediate try {tries} of job {job}.");
info!(context, "begin immediate try {} of job {}", tries, job);
let try_res = match job.action {
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
};
info!(context, "Finished immediate try {tries} of job {job}.");
info!(context, "Finished immediate try {} of job {}", tries, job);
try_res
}
@@ -247,7 +250,7 @@ pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
pub async fn add(context: &Context, job: Job) -> Result<()> {
job.save(context).await.context("failed to save job")?;
info!(context, "Interrupt: IMAP.");
info!(context, "interrupt: imap");
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
@@ -261,7 +264,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
pub(crate) async fn load_next(context: &Context, info: &InterruptInfo) -> Result<Option<Job>> {
info!(context, "Loading job.");
info!(context, "loading job");
let query;
let params;
@@ -277,7 +280,7 @@ WHERE desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = vec![t];
params = paramsv![t];
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
@@ -289,13 +292,13 @@ WHERE tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = vec![];
params = paramsv![];
};
loop {
let job_res = context
.sql
.query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| {
.query_row_optional(query, params.clone(), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
@@ -313,21 +316,19 @@ LIMIT 1;
Ok(job) => return Ok(job),
Err(err) => {
// Remove invalid job from the DB
info!(context, "Cleaning up job, because of {err:#}.");
info!(context, "cleaning up job, because of {:#}", err);
// TODO: improve by only doing a single query
let id = context
.sql
.query_row(query, rusqlite::params_from_iter(params.clone()), |row| {
row.get::<_, i32>(0)
})
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.await
.context("failed to retrieve invalid job ID from the database")?;
.context("Failed to retrieve invalid job ID from the database")?;
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", (id,))
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.await
.with_context(|| format!("failed to delete invalid job {id}"))?;
.with_context(|| format!("Failed to delete invalid job {id}"))?;
}
}
}
@@ -346,7 +347,7 @@ mod tests {
"INSERT INTO jobs
(added_timestamp, action, foreign_id, desired_timestamp)
VALUES (?, ?, ?, ?);",
(
paramsv![
now,
if valid {
Action::DownloadMsg as i32
@@ -354,8 +355,8 @@ mod tests {
-1
},
foreign_id,
now,
),
now
],
)
.await
.unwrap();

Some files were not shown because too many files have changed in this diff Show More