mirror of
https://github.com/chatmail/core.git
synced 2026-04-11 18:12:11 +03:00
Compare commits
1 Commits
rpc-client
...
link2xt/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b06d31190 |
69
.github/workflows/ci.yml
vendored
69
.github/workflows/ci.yml
vendored
@@ -14,7 +14,8 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- stable
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
@@ -24,7 +25,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.73.0
|
||||
RUSTUP_TOOLCHAIN: 1.72.0
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
@@ -38,6 +39,10 @@ jobs:
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
|
||||
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
|
||||
- name: Check musl
|
||||
run: scripts/zig-musl-check.sh
|
||||
|
||||
cargo_deny:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
@@ -76,15 +81,19 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.73.0
|
||||
rust: 1.68.2
|
||||
- os: windows-latest
|
||||
rust: 1.73.0
|
||||
rust: 1.68.2
|
||||
- os: macos-latest
|
||||
rust: 1.73.0
|
||||
rust: 1.68.2
|
||||
|
||||
# Minimum Supported Rust Version = 1.70.0
|
||||
# Minimum Supported Rust Version = 1.65.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.70.0
|
||||
rust: 1.65.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -97,8 +106,6 @@ jobs:
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Test cargo vendor
|
||||
@@ -130,7 +137,7 @@ jobs:
|
||||
name: Build deltachat-rpc-server
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -145,7 +152,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
path: target/debug/deltachat-rpc-server
|
||||
retention-days: 1
|
||||
|
||||
python_lint:
|
||||
@@ -166,8 +173,8 @@ jobs:
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e lint
|
||||
|
||||
cffi_python_tests:
|
||||
name: CFFI Python tests
|
||||
python_tests:
|
||||
name: Python tests
|
||||
needs: ["c_library", "python_lint"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -175,15 +182,15 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.11
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
@@ -217,29 +224,30 @@ jobs:
|
||||
working-directory: python
|
||||
run: tox -e mypy,doc,py
|
||||
|
||||
rpc_python_tests:
|
||||
name: JSON-RPC Python tests
|
||||
aysnc_python_tests:
|
||||
name: Async Python tests
|
||||
needs: ["python_lint", "rpc_server"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
- os: windows-latest
|
||||
python: 3.12
|
||||
python: 3.11
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# 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.7
|
||||
python: 3.8
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -260,18 +268,11 @@ jobs:
|
||||
path: target/debug
|
||||
|
||||
- name: Make deltachat-rpc-server executable
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
run: chmod +x target/debug/deltachat-rpc-server
|
||||
|
||||
- name: Add deltachat-rpc-server to path
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
||||
|
||||
- name: Add deltachat-rpc-server to path
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
run: |
|
||||
"${{ github.workspace }}/target/debug" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Run deltachat-rpc-client tests
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
|
||||
104
.github/workflows/deltachat-rpc-server.yml
vendored
104
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -26,17 +26,35 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install ziglang
|
||||
run: pip install wheel ziglang==0.11.0
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
- name: Build
|
||||
run: sh scripts/zig-rpc-server.sh
|
||||
|
||||
- name: Upload dist directory with Linux binaries
|
||||
- name: Upload x86_64 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: linux
|
||||
path: dist/
|
||||
name: deltachat-rpc-server-x86_64
|
||||
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:
|
||||
name: deltachat-rpc-server-aarch64
|
||||
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload armv7 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7
|
||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
@@ -74,87 +92,39 @@ jobs:
|
||||
|
||||
build_macos:
|
||||
name: Build deltachat-rpc-server for macOS
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
- arch: aarch64
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
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-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Build wheels and upload binaries to the release
|
||||
name: Upload binaries to the release
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download built binaries
|
||||
uses: "actions/download-artifact@v3"
|
||||
|
||||
- name: Download Linux binaries
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux
|
||||
path: dist/
|
||||
|
||||
- name: Download win32 binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-win32.exe
|
||||
path: deltachat-rpc-server-win32.exe.d
|
||||
|
||||
- name: Download win64 binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-win64.exe
|
||||
path: deltachat-rpc-server-win64.exe.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Flatten dist/ directory
|
||||
- name: Compose dist/ directory
|
||||
run: |
|
||||
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
|
||||
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install wheel
|
||||
run: pip install wheel
|
||||
|
||||
- name: Build deltachat-rpc-server Python wheels and source package
|
||||
run: scripts/wheel-rpc-server.py
|
||||
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: List downloaded artifacts
|
||||
run: ls -l dist/
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -2,9 +2,9 @@ name: JSON-RPC API Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
2
.github/workflows/node-docs.yml
vendored
2
.github/workflows/node-docs.yml
vendored
@@ -8,7 +8,7 @@ name: Generate & upload node.js documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
|
||||
2
.github/workflows/node-tests.yml
vendored
2
.github/workflows/node-tests.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
3
.github/workflows/upload-docs.yml
vendored
3
.github/workflows/upload-docs.yml
vendored
@@ -3,7 +3,8 @@ name: Build & Deploy Documentation on rs.delta.chat
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- docs-gh-action
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.github/workflows/upload-ffi-docs.yml
vendored
3
.github/workflows/upload-ffi-docs.yml
vendored
@@ -7,7 +7,8 @@ name: Build & Deploy Documentation on cffi.delta.chat
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- docs-gh-action
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/build
|
||||
/dist
|
||||
|
||||
# ignore vi temporaries
|
||||
*~
|
||||
@@ -19,9 +18,6 @@ python/.eggs
|
||||
__pycache__
|
||||
python/src/deltachat/capi*.so
|
||||
python/.venv/
|
||||
python/venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
python/liveconfig*
|
||||
|
||||
|
||||
210
CHANGELOG.md
210
CHANGELOG.md
@@ -1,208 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.127.0] - 2023-10-26
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] `dc_accounts_new` API is changed. Unused `os_name` argument is removed and `writable` argument is added.
|
||||
- jsonrpc: Add `resend_messages`.
|
||||
- [**breaking**] Remove unused function `is_verified_ex()` ([#4551](https://github.com/deltachat/deltachat-core-rust/pull/4551))
|
||||
- [**breaking**] Make `MsgId.delete_from_db()` private.
|
||||
- [**breaking**] deltachat-jsonrpc: use `kind` as a tag for all union types
|
||||
- json-rpc: Force stickers to be sent as stickers ([#4819](https://github.com/deltachat/deltachat-core-rust/pull/4819)).
|
||||
- Add mailto parse api ([#4829](https://github.com/deltachat/deltachat-core-rust/pull/4829)).
|
||||
- [**breaking**] Remove unused `DC_STR_PROTECTION_(EN)ABLED` strings
|
||||
- [**breaking**] Remove unused `dc_set_chat_protection()`
|
||||
- Hide `DcSecretKey` trait from the API.
|
||||
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
|
||||
|
||||
### CI
|
||||
|
||||
- Run Rust tests with `RUST_BACKTRACE` set.
|
||||
- Replace `master` branch with `main`. Run CI only on `main` branch pushes.
|
||||
- Test `deltachat-rpc-client` on Windows.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document how logs and error messages should be formatted in `CONTRIBUTING.md`.
|
||||
- Clarify transitive behaviour of `dc_contact_is_verfified()`.
|
||||
- Document `configured_addr`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add lockfile to account manager ([#4314](https://github.com/deltachat/deltachat-core-rust/pull/4314)).
|
||||
- Don't show a contact as verified if their key changed since the verification ([#4574](https://github.com/deltachat/deltachat-core-rust/pull/4574)).
|
||||
- deltachat-rpc-server: Add `--openrpc` option to print OpenRPC specification for JSON-RPC API. This specification can be used to generate JSON-RPC API clients.
|
||||
- Track whether contact is a bot or not ([#4821](https://github.com/deltachat/deltachat-core-rust/pull/4821)).
|
||||
- Replace `Config::SendSyncMsgs` with `SyncMsgs` ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt ([#4538](https://github.com/deltachat/deltachat-core-rust/pull/4538)).
|
||||
- Allow to save a draft if the verification is broken ([#4542](https://github.com/deltachat/deltachat-core-rust/pull/4542)).
|
||||
- Fix info-message orderings of verified 1:1 chats ([#4545](https://github.com/deltachat/deltachat-core-rust/pull/4545)).
|
||||
- Fix example; this was changed some time ago, see https://docs.webxdc.org/spec.html#sendupdate
|
||||
- `receive_imf`: Update peerstate from db after handling Securejoin handshake ([#4600](https://github.com/deltachat/deltachat-core-rust/pull/4600)).
|
||||
- Sort old incoming messages below all outgoing ones ([#4621](https://github.com/deltachat/deltachat-core-rust/pull/4621)).
|
||||
- Do not mark non-verified group chats as verified when using securejoin.
|
||||
- `receive_imf`: Set protection only for Chattype::Single ([#4597](https://github.com/deltachat/deltachat-core-rust/pull/4597)).
|
||||
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
|
||||
- Clear VerifiedOneOnOneChats config on backup ([#4615](https://github.com/deltachat/deltachat-core-rust/pull/4615)).
|
||||
- Try removal of accounts multiple times with timeouts in case the database file is blocked (restore `try_many_times` workaround).
|
||||
|
||||
### Build system
|
||||
|
||||
- Remove examples/simple.rs.
|
||||
- Increase MSRV to 1.70.0.
|
||||
- Update dependencies.
|
||||
- Switch to iroh 0.4.x fork with updated dependencies.
|
||||
|
||||
## [1.126.1] - 2023-10-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not hardcode version in deltachat-rpc-server source package.
|
||||
- Do not interrupt IMAP loop from `get_connectivity_html()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- imap: Buffer `STARTTLS` command.
|
||||
|
||||
### Build system
|
||||
|
||||
- Build `deltachat-rpc-server` binary for aarch64 macOS.
|
||||
- Build `deltachat-rpc-server` wheels for macOS and Windows.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove job queue.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Update `ahash` to make `cargo-deny` happy.
|
||||
|
||||
## [1.126.0] - 2023-10-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
|
||||
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
|
||||
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
|
||||
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
|
||||
|
||||
### CI
|
||||
|
||||
- Build Python wheels for deltachat-rpc-server.
|
||||
|
||||
### Build system
|
||||
|
||||
- Strip release binaries.
|
||||
- Workaround OpenSSL crate expecting libatomic to be available.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set `soft_heap_limit` on SQLite database.
|
||||
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
|
||||
|
||||
## [1.125.0] - 2023-10-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads.
|
||||
- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error.
|
||||
|
||||
### CI
|
||||
|
||||
- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add developer option to disable IDLE.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`.
|
||||
- python: Don't automatically set the displayname to "bot" when setting log level.
|
||||
- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)).
|
||||
- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)).
|
||||
- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)).
|
||||
- Set connectivity status to "connected" during fake idle.
|
||||
- Return verifier contacts regardless of their origin.
|
||||
- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`.
|
||||
- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead.
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: Enable logs in pytest.
|
||||
|
||||
## [1.124.1] - 2023-10-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
|
||||
|
||||
### CI
|
||||
|
||||
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
|
||||
|
||||
## [1.124.0] - 2023-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
|
||||
- Deprecate `dc_contact_get_verifier_addr`.
|
||||
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
|
||||
- Deprecate `get_next_media()`.
|
||||
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Wrap base64-encoded parts to 76 characters.
|
||||
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
|
||||
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
|
||||
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
|
||||
- Do not put the status footer into reaction MIME parts.
|
||||
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
|
||||
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
|
||||
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
|
||||
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
|
||||
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
|
||||
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
|
||||
- deltachat-rpc-client: Log exceptions when long-running tasks die.
|
||||
|
||||
### Build
|
||||
|
||||
- Build wheels for Python 3.12 and PyPy 3.10.
|
||||
|
||||
## [1.123.0] - 2023-09-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
|
||||
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- imex: Use "default" in the filename of the default key.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update OpenSSL from 3.1.2 to 3.1.3.
|
||||
|
||||
## [1.122.0] - 2023-09-12
|
||||
|
||||
### API-Changes
|
||||
@@ -3019,10 +2816,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
|
||||
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
|
||||
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
|
||||
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
||||
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
|
||||
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
|
||||
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
|
||||
[1.126.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.0...v1.126.1
|
||||
[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0
|
||||
|
||||
@@ -76,29 +76,6 @@ If you have multiple changes in one PR, create multiple conventional commits, an
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
### Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
||||
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
||||
For example:
|
||||
```
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
Log messages should be capitalized and have a full stop in the end. For example:
|
||||
```
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
```
|
||||
|
||||
Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||
```
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
```
|
||||
|
||||
### Reviewing
|
||||
|
||||
Once a PR has an approval and passes CI, it can be merged.
|
||||
|
||||
1414
Cargo.lock
generated
1414
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.127.0"
|
||||
version = "1.122.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.65"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -24,7 +24,10 @@ lto = true
|
||||
panic = 'abort'
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[patch.crates-io]
|
||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
@@ -39,20 +42,18 @@ async-smtp = { version = "0.9", default-features = false, features = ["runtime-t
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.21"
|
||||
brotli = { version = "3.4", default-features=false, features = ["std"] }
|
||||
brotli = { version = "3.3", default-features=false, features = ["std"] }
|
||||
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"
|
||||
fd-lock = "3.0.11"
|
||||
futures = "0.3"
|
||||
futures-lite = "2.0.0"
|
||||
futures-lite = "1.13.0"
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { git = "https://github.com/deltachat/iroh", branch = "0.4-update-quic", default-features = false }
|
||||
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { version = "0.4.1", default-features = false }
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
@@ -67,13 +68,13 @@ parking_lot = "0.12"
|
||||
pgp = { version = "0.10", default-features = false }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quick-xml = "0.29"
|
||||
rand = "0.8"
|
||||
regex = "1.9"
|
||||
reqwest = { version = "0.11.20", features = ["json"] }
|
||||
regex = "1.8"
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
sanitize-filename = "0.4"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
@@ -88,15 +89,16 @@ tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
tokio-util = "0.7.8"
|
||||
toml = "0.7"
|
||||
trust-dns-resolver = "0.22"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.0.0"
|
||||
futures-lite = "1.13"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
@@ -116,6 +118,11 @@ members = [
|
||||
"format-flowed",
|
||||
]
|
||||
|
||||
[[example]]
|
||||
name = "simple"
|
||||
path = "examples/simple.rs"
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "create_account"
|
||||
harness = false
|
||||
|
||||
14
README.md
14
README.md
@@ -1,16 +1,8 @@
|
||||
<p align="center">
|
||||
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||
</p>
|
||||
# Delta Chat Rust
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
> Deltachat-core written in Rust
|
||||
|
||||
<p align="center">
|
||||
The core library for Delta Chat, written in Rust
|
||||
</p>
|
||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
|
||||
@@ -54,7 +54,7 @@ header = """
|
||||
# Changelog\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#templates
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.127.0"
|
||||
version = "1.122.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
|
||||
# exclude all test directories use the pattern */test/*
|
||||
|
||||
######################################################
|
||||
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
|
||||
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
|
||||
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
||||
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
||||
######################################################
|
||||
|
||||
@@ -384,12 +384,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
/**
|
||||
* Configure the context. The configuration is handled by key=value pairs as:
|
||||
*
|
||||
* - `addr` = Email address to use for configuration.
|
||||
* If dc_configure() fails this is not the email address actually in use.
|
||||
* Use `configured_addr` to find out the email address actually in use.
|
||||
* - `configured_addr` = Email address actually in use.
|
||||
* Unless for testing, do not set this value using dc_set_config().
|
||||
* Instead, set `addr` and call dc_configure().
|
||||
* - `addr` = address to display (always needed)
|
||||
* - `mail_server` = IMAP-server, guessed if left out
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
@@ -497,9 +492,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `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.
|
||||
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
|
||||
* 0=use IMAP IDLE if the server supports it.
|
||||
* This is a developer option used for testing polling used as an IDLE fallback.
|
||||
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
||||
* For larger messages, only the header is downloaded and a placeholder is shown.
|
||||
* These messages can be downloaded fully using dc_download_full_msg() later.
|
||||
@@ -508,16 +500,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* to not mess up with non-delivery-reports or read-receipts.
|
||||
* 0=no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||
* to 1 if it supports verified 1:1 chats.
|
||||
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||
* and when the key changes, an info message is posted into the chat.
|
||||
* 0=Nothing else happens when the key changes.
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||
* The prefix should be followed by the system and maybe subsystem,
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
@@ -842,7 +824,7 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The e-mail address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data Ignored, actual public key is extracted from secret_data.
|
||||
* @param public_data ASCII armored public key.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
@@ -896,8 +878,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||
* is added as needed.
|
||||
* @param query_str An optional query for filtering the list. Only chats matching this query
|
||||
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
|
||||
* the chatlist is filtered such that only chats with unread messages show up.
|
||||
* are returned. Give NULL for no filtering.
|
||||
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
||||
* are returned. Give 0 for no filtering.
|
||||
* @return A chatlist as an dc_chatlist_t object.
|
||||
@@ -1137,7 +1118,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
||||
*
|
||||
* In JS land, that would be mapped to something as:
|
||||
* ```
|
||||
* success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
|
||||
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
|
||||
* ```
|
||||
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
|
||||
* See dc_get_webxdc_status_updates() for the receiving counterpart.
|
||||
@@ -1194,6 +1175,24 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
|
||||
*/
|
||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
||||
|
||||
|
||||
/**
|
||||
* Replaces webxdc app with a new version.
|
||||
*
|
||||
* On the JavaScript side this API could be used like this:
|
||||
* ```
|
||||
* window.webxdc.replaceWebxdc(blob);
|
||||
* ```
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the WebXDC message to be replaced.
|
||||
* @param blob New blob to replace WebXDC with.
|
||||
* @param n Blob size.
|
||||
*/
|
||||
void dc_replace_webxdc(dc_context_t* context, uint32_t msg_id, uint8_t *blob, size_t n);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -1500,7 +1499,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||
@@ -1519,6 +1517,24 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
|
||||
|
||||
|
||||
/**
|
||||
* Enable or disable protection against active attacks.
|
||||
* To enable protection, it is needed that all members are verified;
|
||||
* if this condition is met, end-to-end-encryption is always enabled
|
||||
* and only the verified keys are used.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
|
||||
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat to change the protection for.
|
||||
* @param protect 1=protect chat, 0=unprotect chat
|
||||
* @return 1=success, 0=error, e.g. some members may be unverified
|
||||
*/
|
||||
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
|
||||
|
||||
|
||||
/**
|
||||
* Set chat visibility to pinned, archived or normal.
|
||||
*
|
||||
@@ -1712,12 +1728,24 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
|
||||
* Create a new broadcast list.
|
||||
*
|
||||
* Broadcast lists are similar to groups on the sending device,
|
||||
* however, recipients get the messages in a read-only chat
|
||||
* and will see who the other members are.
|
||||
* however, recipients get the messages in normal one-to-one chats
|
||||
* and will not be aware of other members.
|
||||
*
|
||||
* For historical reasons, this function does not take a name directly,
|
||||
* instead you have to set the name using dc_set_chat_name()
|
||||
* after creating the broadcast list.
|
||||
* Replies to broadcasts go only to the sender
|
||||
* and not to all broadcast recipients.
|
||||
* Moreover, replies will not appear in the broadcast list
|
||||
* but in the one-to-one chat with the person answering.
|
||||
*
|
||||
* The name and the image of the broadcast list is set automatically
|
||||
* and is visible to the sender only.
|
||||
* Not asking for these data allows more focused creation
|
||||
* and we bypass the question who will get which data.
|
||||
* Also, many users will have at most one broadcast list
|
||||
* so, a generic name and image is sufficient at the first place.
|
||||
*
|
||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
* All in all, this is also what other messengers are doing here.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -2260,7 +2288,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
* the backup is not encrypted.
|
||||
* The backup contains all contacts, chats, images and other data and device independent settings.
|
||||
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
|
||||
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
||||
* the format is `delta-chat-<day>-<number>.tar`
|
||||
*
|
||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
||||
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||
@@ -2945,15 +2974,12 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
||||
* @param dir The directory to create the context-databases in.
|
||||
* If the directory does not exist,
|
||||
* dc_accounts_new() will try to create it.
|
||||
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
|
||||
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
|
||||
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
|
||||
* @return An account manager object.
|
||||
* The object must be passed to the other account manager functions
|
||||
* and must be freed using dc_accounts_unref() after usage.
|
||||
* On errors, NULL is returned.
|
||||
*/
|
||||
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
|
||||
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3734,6 +3760,7 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
* Check if a chat is protected.
|
||||
* Protected chats contain only verified members and encryption is always enabled.
|
||||
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
||||
* The status can be changed using dc_set_chat_protection().
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -3742,26 +3769,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||
*
|
||||
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
* otherwise it will return false for all chats.
|
||||
*
|
||||
* 1:1 chats are automatically set as protected when a contact is verified.
|
||||
* When a message comes in that is not encrypted / signed correctly,
|
||||
* the chat is automatically set as unprotected again.
|
||||
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
|
||||
*
|
||||
* The UI should let the user confirm that this is OK with a message like
|
||||
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protection broken, 0=otherwise.
|
||||
*/
|
||||
int dc_chat_is_protection_broken (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if locations are sent to the chat
|
||||
* at the time the object was created using dc_get_chat().
|
||||
@@ -3966,7 +3973,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
|
||||
* Get the message time used for sorting.
|
||||
* This function returns the timestamp that is used for sorting the message
|
||||
* into lists as returned e.g. by dc_get_chat_msgs().
|
||||
* This may be the received time, the sending time or another time.
|
||||
* This may be the reveived time, the sending time or another time.
|
||||
*
|
||||
* To get the receiving time, use dc_msg_get_received_timestamp().
|
||||
* To get the sending time, use dc_msg_get_timestamp().
|
||||
@@ -4356,7 +4363,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
|
||||
* Check if the message is an informational message, created by the
|
||||
* device or by another users. Such messages are not "typed" by the user but
|
||||
* created due to other actions,
|
||||
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
|
||||
* or dc_add_contact_to_chat().
|
||||
*
|
||||
* These messages are typically shown in the center of the chat view,
|
||||
@@ -5050,12 +5057,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
/**
|
||||
* Return the address that verified a contact
|
||||
*
|
||||
* The UI may use this in addition to a checkmark showing the verification status.
|
||||
* In case of verification chains,
|
||||
* the last contact in the chain is shown.
|
||||
* This is because of privacy reasons, but also as it would not help the user
|
||||
* to see a unknown name here - where one can mostly always ask the shown name
|
||||
* as it is directly known.
|
||||
* The UI may use this in addition to a checkmark showing the verification status
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5063,7 +5065,6 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||
* information or the contact is not verified.
|
||||
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||
*/
|
||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
|
||||
@@ -5076,7 +5077,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||
* the contact is not verified.
|
||||
*/
|
||||
@@ -5772,11 +5773,12 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param input JSON-RPC request.
|
||||
* @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.
|
||||
* If there is no response, NULL is returned.
|
||||
* On error, NULL is returned.
|
||||
*/
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
@@ -6796,6 +6798,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in error strings.
|
||||
#define DC_STR_ERROR_NO_NETWORK 87
|
||||
|
||||
/// "Chat protection enabled."
|
||||
///
|
||||
|
||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
|
||||
#define DC_STR_PROTECTION_ENABLED 88
|
||||
|
||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
|
||||
#define DC_STR_PROTECTION_DISABLED 89
|
||||
|
||||
/// "Reply"
|
||||
///
|
||||
/// Used in summaries.
|
||||
@@ -7240,6 +7251,26 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||
|
||||
/// "You enabled chat protection."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
|
||||
|
||||
/// "Chat protection enabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
|
||||
|
||||
/// "You disabled chat protection."
|
||||
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
|
||||
|
||||
/// "Chat protection disabled by %1$s."
|
||||
///
|
||||
/// `%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.
|
||||
@@ -7250,16 +7281,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
|
||||
/// "%1$s sent a message from another device."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -29,14 +29,14 @@ use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
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;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial};
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
@@ -805,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
_public_data: *const libc::c_char,
|
||||
public_data: *const libc::c_char,
|
||||
secret_data: *const libc::c_char,
|
||||
) -> i32 {
|
||||
if context.is_null() {
|
||||
@@ -813,12 +813,21 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let addr = to_string_lossy(addr);
|
||||
let secret_data = to_string_lossy(secret_data);
|
||||
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
|
||||
.context("Failed to save keypair")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
block_on(async move {
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
};
|
||||
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
|
||||
Ok::<_, anyhow::Error>(1)
|
||||
})
|
||||
.context("Failed to save keypair")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1088,6 +1097,32 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_replace_webxdc(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
blob: *const u8,
|
||||
n: libc::size_t,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_replace_webxdc()");
|
||||
return;
|
||||
}
|
||||
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
let blob_slice = std::slice::from_raw_parts(blob, n);
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
replace_webxdc(ctx, msg_id, blob_slice)
|
||||
.await
|
||||
.context("Failed to replace WebXDC")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1422,7 +1457,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(deprecated)]
|
||||
pub unsafe extern "C" fn dc_get_next_media(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
@@ -1463,6 +1497,32 @@ pub unsafe extern "C" fn dc_get_next_media(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_protection(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
protect: libc::c_int,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_protection()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
|
||||
s
|
||||
} else {
|
||||
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
|
||||
return 0;
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
match ChatId::new(chat_id).set_protection(ctx, protect).await {
|
||||
Ok(()) => 1,
|
||||
Err(_) => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1505,14 +1565,10 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.delete(ctx)
|
||||
.await
|
||||
.context("Failed chat delete")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
block_on(ChatId::new(chat_id).delete(ctx))
|
||||
.context("Failed chat delete")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2533,12 +2589,7 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3097,16 +3148,6 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_protection_broken() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
@@ -4512,14 +4553,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
true,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -4546,14 +4580,11 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
match block_on(provider::get_provider_info(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -4725,17 +4756,17 @@ pub type dc_accounts_t = AccountsWrapper;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
dir: *const libc::c_char,
|
||||
writable: libc::c_int,
|
||||
_os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> *mut dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dir.is_null() {
|
||||
if dbfile.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_new()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||
let accs = block_on(Accounts::new(as_path(dbfile).into()));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
@@ -5001,7 +5032,7 @@ 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, RpcSession};
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -5077,24 +5108,25 @@ mod jsonrpc {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
input: *const libc::c_char,
|
||||
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 input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
let method = to_string_lossy(method);
|
||||
let params = to_string_lossy(params);
|
||||
let params: Option<yerpc::Params> = match serde_json::from_str(¶ms) {
|
||||
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 {
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
Ok(res) => res.to_string().strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.127.0"
|
||||
version = "1.122.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -15,26 +15,26 @@ required-features = ["webserver"]
|
||||
anyhow = "1"
|
||||
deltachat = { path = ".." }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.13"
|
||||
schemars = "0.8.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
tempfile = "3.6.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.28" }
|
||||
serde_json = "1.0.105"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||
tokio = { version = "1.33.0" }
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1.0.99"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.29.1" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.6.20", optional = true, features = ["ws"] }
|
||||
axum = { version = "0.6.18", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,30 +4,30 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::Config;
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
use deltachat::qr::Qr;
|
||||
use deltachat::{
|
||||
chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
ProtectionStatus,
|
||||
},
|
||||
chatlist::Chatlist,
|
||||
config::Config,
|
||||
constants::DC_MSG_ID_DAYMARKER,
|
||||
contact::{may_be_valid_addr, Contact, ContactId, Origin},
|
||||
context::get_info,
|
||||
ephemeral::Timer,
|
||||
imex, location,
|
||||
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
|
||||
provider::get_provider_info,
|
||||
qr,
|
||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
||||
reaction::{get_msg_reactions, send_reaction},
|
||||
securejoin,
|
||||
stock_str::StockMessage,
|
||||
webxdc::StatusUpdateSerial,
|
||||
};
|
||||
use deltachat::provider::get_provider_info;
|
||||
use deltachat::qr::{self, Qr};
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
@@ -142,7 +142,11 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
#[rpc(
|
||||
all_positional,
|
||||
ts_outdir = "typescript/generated",
|
||||
openrpc_outdir = "openrpc"
|
||||
)]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -153,12 +157,12 @@ impl CommandApi {
|
||||
// Misc top level functions
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Checks if an email address is valid.
|
||||
/// Check if an email address is valid.
|
||||
async fn check_email_validity(&self, email: String) -> bool {
|
||||
may_be_valid_addr(&email)
|
||||
}
|
||||
|
||||
/// Returns general system info.
|
||||
/// Get general system info.
|
||||
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
||||
get_info()
|
||||
}
|
||||
@@ -219,13 +223,11 @@ impl CommandApi {
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// Starts background tasks for all accounts.
|
||||
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
||||
self.accounts.read().await.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops background tasks for all accounts.
|
||||
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_io().await;
|
||||
Ok(())
|
||||
@@ -235,16 +237,14 @@ impl CommandApi {
|
||||
// Methods that work on individual accounts
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Starts background tasks for a single account.
|
||||
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
async fn start_io(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(id).await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops background tasks for a single account.
|
||||
async fn stop_io(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
async fn stop_io(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(id).await?;
|
||||
ctx.stop_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -311,13 +311,11 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
/// Sets the given configuration key.
|
||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
set_config(&ctx, &key, value.as_deref()).await
|
||||
}
|
||||
|
||||
/// Updates a batch of configuration values.
|
||||
async fn batch_set_config(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -349,7 +347,6 @@ impl CommandApi {
|
||||
Ok(qr_object)
|
||||
}
|
||||
|
||||
/// Returns configuration value for the given key.
|
||||
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_config(&ctx, &key).await
|
||||
@@ -815,12 +812,24 @@ impl CommandApi {
|
||||
/// Create a new broadcast list.
|
||||
///
|
||||
/// Broadcast lists are similar to groups on the sending device,
|
||||
/// however, recipients get the messages in a read-only chat
|
||||
/// and will see who the other members are.
|
||||
/// however, recipients get the messages in normal one-to-one chats
|
||||
/// and will not be aware of other members.
|
||||
///
|
||||
/// For historical reasons, this function does not take a name directly,
|
||||
/// instead you have to set the name using dc_set_chat_name()
|
||||
/// after creating the broadcast list.
|
||||
/// Replies to broadcasts go only to the sender
|
||||
/// and not to all broadcast recipients.
|
||||
/// Moreover, replies will not appear in the broadcast list
|
||||
/// but in the one-to-one chat with the person answering.
|
||||
///
|
||||
/// The name and the image of the broadcast list is set automatically
|
||||
/// and is visible to the sender only.
|
||||
/// Not asking for these data allows more focused creation
|
||||
/// and we bypass the question who will get which data.
|
||||
/// Also, many users will have at most one broadcast list
|
||||
/// so, a generic name and image is sufficient at the first place.
|
||||
///
|
||||
/// Later on, however, the name can be changed using dc_set_chat_name().
|
||||
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
/// All in all, this is also what other messengers are doing here.
|
||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_broadcast_list(&ctx)
|
||||
@@ -1418,10 +1427,6 @@ impl CommandApi {
|
||||
///
|
||||
/// one combined call for getting chat::get_next_media for both directions
|
||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
///
|
||||
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||
/// and navigate the returned array instead.
|
||||
#[allow(deprecated)]
|
||||
async fn get_neighboring_chat_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1744,9 +1749,6 @@ impl CommandApi {
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(&sticker_path, None);
|
||||
|
||||
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||
msg.force_sticker();
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
@@ -1880,7 +1882,7 @@ impl CommandApi {
|
||||
.context("path conversion to string failed")
|
||||
}
|
||||
|
||||
/// Saves a sticker to a collection/folder in the account's sticker folder.
|
||||
/// save a sticker to a collection/folder in the account's sticker folder
|
||||
async fn misc_save_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2059,23 +2061,6 @@ impl CommandApi {
|
||||
|
||||
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
||||
}
|
||||
|
||||
// send the chat's current set draft
|
||||
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||
let mut draft = draft;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
|
||||
.await?
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions (to prevent code duplication)
|
||||
|
||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Account {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Configured {
|
||||
|
||||
@@ -167,11 +167,10 @@ impl BasicChat {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
Until { duration: i64 },
|
||||
Until(i64),
|
||||
}
|
||||
|
||||
impl MuteDuration {
|
||||
@@ -179,13 +178,13 @@ impl MuteDuration {
|
||||
match self {
|
||||
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
||||
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
||||
MuteDuration::Until { duration } => {
|
||||
if duration <= 0 {
|
||||
MuteDuration::Until(n) => {
|
||||
if n <= 0 {
|
||||
bail!("failed to read mute duration")
|
||||
}
|
||||
|
||||
Ok(SystemTime::now()
|
||||
.checked_add(Duration::from_secs(duration as u64))
|
||||
.checked_add(Duration::from_secs(n as u64))
|
||||
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatListItemFetchResult {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatListItem {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl From<CoreEvent> for Event {
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::reactions::JSONRPCReactions;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
||||
pub enum MessageLoadResult {
|
||||
Message(MessageObject),
|
||||
LoadingError { error: String },
|
||||
|
||||
@@ -4,7 +4,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum QrObject {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
|
||||
@@ -13,8 +13,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
@@ -55,8 +54,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
@@ -19,8 +19,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
|
||||
@@ -35,7 +35,7 @@ async function run() {
|
||||
const accounts = await client.rpc.getAllAccounts();
|
||||
console.log("accounts loaded", accounts);
|
||||
for (const account of accounts) {
|
||||
if (account.kind === "Configured") {
|
||||
if (account.type === "Configured") {
|
||||
write(
|
||||
$head,
|
||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||
@@ -57,7 +57,7 @@ async function run() {
|
||||
clear($main);
|
||||
const selectedAccount = SELECTED_ACCOUNT;
|
||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||
if (info.kind !== "Configured") {
|
||||
if (info.type !== "Configured") {
|
||||
return write($main, "Account is not configured");
|
||||
}
|
||||
write($main, `<h2>${info.addr!}</h2>`);
|
||||
@@ -81,7 +81,8 @@ async function run() {
|
||||
messageIds
|
||||
);
|
||||
for (const [_messageId, message] of Object.entries(messages)) {
|
||||
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||
if (message.variant === "message")
|
||||
write($main, `<p>${message.text}</p>`);
|
||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||
}
|
||||
}
|
||||
@@ -92,9 +93,9 @@ async function run() {
|
||||
$side,
|
||||
`
|
||||
<p class="message">
|
||||
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(
|
||||
Object.assign({}, event, { kind: undefined })
|
||||
Object.assign({}, event, { type: undefined })
|
||||
)}
|
||||
</p>`
|
||||
);
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.127.0"
|
||||
"version": "1.122.0"
|
||||
}
|
||||
|
||||
@@ -6,22 +6,22 @@ import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
[Property in EventType["type"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
event: Extract<EventType, { type: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
[Property in EventType["type"]]: (
|
||||
event: Extract<EventType, { type: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type DcEvent = EventType;
|
||||
export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||
export type DcEventType<T extends EventType["type"]> = Extract<
|
||||
EventType,
|
||||
{ kind: T }
|
||||
{ type: T }
|
||||
>;
|
||||
|
||||
export class BaseDeltaChat<
|
||||
@@ -46,12 +46,12 @@ export class BaseDeltaChat<
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
//@ts-ignore
|
||||
this.emit(event.event.kind, event.contextId, event.event);
|
||||
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.kind,
|
||||
event.event.type,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
);
|
||||
|
||||
@@ -29,8 +29,8 @@ describe("online tests", function () {
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||
|
||||
dc.on("ALL", (contextId, { kind }) => {
|
||||
if (kind !== "Info") console.log(contextId, kind);
|
||||
dc.on("ALL", (contextId, { type }) => {
|
||||
if (type !== "Info") console.log(contextId, type);
|
||||
});
|
||||
|
||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
||||
@@ -148,7 +148,7 @@ describe("online tests", function () {
|
||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||
]);
|
||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||
// Check if answer arrives at A and if it is encrypted
|
||||
// Check if answer arives at A and if it is encrypted
|
||||
await eventPromise2;
|
||||
|
||||
const messageId = (
|
||||
@@ -177,12 +177,12 @@ describe("online tests", function () {
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||
async function waitForEvent<T extends DcEvent["type"]>(
|
||||
dc: DeltaChat,
|
||||
eventType: T,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||
): Promise<Extract<DcEvent, { type: T }>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rejectTimeout = setTimeout(
|
||||
() => reject(new Error("Timeout reached before event came in")),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.127.0"
|
||||
version = "1.122.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -9,7 +9,7 @@ ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.20"
|
||||
log = "0.4.19"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.29"
|
||||
rustyline = "12"
|
||||
|
||||
@@ -18,7 +18,6 @@ use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::reaction::send_reaction;
|
||||
@@ -139,7 +138,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
/* import a directory */
|
||||
let dir_name = std::path::Path::new(&real_spec);
|
||||
let dir = fs::read_dir(dir_name).await;
|
||||
if let Ok(mut dir) = dir {
|
||||
if dir.is_err() {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
||||
return false;
|
||||
} else {
|
||||
let mut dir = dir.unwrap();
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
@@ -151,9 +154,6 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
@@ -211,17 +211,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
} else {
|
||||
"[FRESH]"
|
||||
},
|
||||
if msg.is_info() {
|
||||
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
|
||||
"[INFO 🛡️]"
|
||||
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
|
||||
"[INFO 🛡️❌]"
|
||||
} else {
|
||||
"[INFO]"
|
||||
}
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if msg.is_info() { "[INFO]" } else { "" },
|
||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
@@ -406,6 +396,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
unpin <chat-id>\n\
|
||||
mute <chat-id> [<seconds>]\n\
|
||||
unmute <chat-id>\n\
|
||||
protect <chat-id>\n\
|
||||
unprotect <chat-id>\n\
|
||||
delchat <chat-id>\n\
|
||||
accept <chat-id>\n\
|
||||
decline <chat-id>\n\
|
||||
@@ -902,7 +894,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let latitude = arg1.parse()?;
|
||||
let longitude = arg2.parse()?;
|
||||
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
||||
if continue_streaming {
|
||||
println!("Success, streaming should be continued.");
|
||||
} else {
|
||||
@@ -1080,6 +1072,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
};
|
||||
chat::set_muted(&context, chat_id, duration).await?;
|
||||
}
|
||||
"protect" | "unprotect" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id
|
||||
.set_protection(
|
||||
&context,
|
||||
match arg0 {
|
||||
"protect" => ProtectionStatus::Protected,
|
||||
"unprotect" => ProtectionStatus::Unprotected,
|
||||
_ => unreachable!("arg0={:?}", arg0),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"delchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
|
||||
@@ -37,18 +37,19 @@ $ tox --devenv env
|
||||
$ . env/bin/activate
|
||||
```
|
||||
|
||||
```
|
||||
$ python
|
||||
>>> from deltachat_rpc_client import *
|
||||
>>> rpc = Rpc()
|
||||
>>> rpc.start()
|
||||
>>> dc = DeltaChat(rpc)
|
||||
>>> system_info = dc.get_system_info()
|
||||
>>> system_info["level"]
|
||||
'awesome'
|
||||
>>> rpc.close()
|
||||
```
|
||||
It is recommended to use IPython, because it supports using `await` directly
|
||||
from the REPL.
|
||||
|
||||
## Usage from async code
|
||||
|
||||
See the [echobot_async.py](./examples/echobot_async.py) example.
|
||||
```
|
||||
$ pip install ipython
|
||||
$ PATH="../target/debug:$PATH" ipython
|
||||
...
|
||||
In [1]: from deltachat_rpc_client import *
|
||||
In [2]: rpc = Rpc()
|
||||
In [3]: await rpc.start()
|
||||
In [4]: dc = DeltaChat(rpc)
|
||||
In [5]: system_info = await dc.get_system_info()
|
||||
In [6]: system_info["level"]
|
||||
Out[6]: 'awesome'
|
||||
In [7]: await rpc.close()
|
||||
```
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||
Pass --help to the CLI to see available options.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from deltachat_rpc_client import events, run_bot_cli
|
||||
|
||||
hooks = events.HookCollection()
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
def log_event(event):
|
||||
async def log_event(event):
|
||||
print(event)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage)
|
||||
def echo(event):
|
||||
async def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_bot_cli(hooks)
|
||||
asyncio.run(run_bot_cli(hooks))
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
it will echo back any message that has non-empty text and also supports the /help command.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||
|
||||
@@ -13,62 +13,62 @@ hooks = events.HookCollection()
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
def log_event(event):
|
||||
if event.kind == EventType.INFO:
|
||||
async def log_event(event):
|
||||
if event.type == EventType.INFO:
|
||||
logging.info(event.msg)
|
||||
elif event.kind == EventType.WARNING:
|
||||
elif event.type == EventType.WARNING:
|
||||
logging.warning(event.msg)
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent(EventType.ERROR))
|
||||
def log_error(event):
|
||||
async def log_error(event):
|
||||
logging.error(event.msg)
|
||||
|
||||
|
||||
@hooks.on(events.MemberListChanged)
|
||||
def on_memberlist_changed(event):
|
||||
async def on_memberlist_changed(event):
|
||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupImageChanged)
|
||||
def on_group_image_changed(event):
|
||||
async def on_group_image_changed(event):
|
||||
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupNameChanged)
|
||||
def on_group_name_changed(event):
|
||||
async def on_group_name_changed(event):
|
||||
logging.info("group name changed, old name: %s", event.old_name)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
||||
def echo(event):
|
||||
async def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
if snapshot.text or snapshot.file:
|
||||
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(command="/help"))
|
||||
def help_command(event):
|
||||
async def help_command(event):
|
||||
snapshot = event.message_snapshot
|
||||
snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
await snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
|
||||
|
||||
def main():
|
||||
with Rpc() as rpc:
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = deltachat.get_system_info()
|
||||
system_info = await deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
||||
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
|
||||
bot = Bot(account, hooks)
|
||||
if not bot.is_configured():
|
||||
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
|
||||
configure_thread.start()
|
||||
bot.run_forever()
|
||||
if not await bot.is_configured():
|
||||
# Save a reference to avoid garbage collection of the task.
|
||||
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
|
||||
await bot.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
main()
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example asynchronous echo bot
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import EventType, Rpc, SpecialContactId
|
||||
|
||||
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
system_info = await rpc.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
account_ids = await rpc.get_all_account_ids()
|
||||
accid = account_ids[0] if account_ids else await rpc.add_account()
|
||||
|
||||
await rpc.set_config(accid, "bot", "1")
|
||||
if not await rpc.is_configured(accid):
|
||||
logging.info("Account is not configured, configuring")
|
||||
await rpc.set_config(accid, "addr", sys.argv[1])
|
||||
await rpc.set_config(accid, "mail_pw", sys.argv[2])
|
||||
await rpc.configure(accid)
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
await rpc.start_io(accid)
|
||||
|
||||
async def process_messages():
|
||||
for msgid in await rpc.get_next_msgs(accid):
|
||||
msg = await rpc.get_message(accid, msgid)
|
||||
if msg["from_id"] != SpecialContactId.SELF and not msg["is_bot"] and not msg["is_info"]:
|
||||
await rpc.misc_send_text_message(accid, msg["chat_id"], msg["text"])
|
||||
await rpc.markseen_msgs(accid, [msgid])
|
||||
|
||||
# Process old messages.
|
||||
await process_messages()
|
||||
|
||||
while True:
|
||||
event = await rpc.wait_for_event(accid)
|
||||
if event["kind"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["kind"] == EventType.WARNING:
|
||||
logging.warning("%s", event["msg"])
|
||||
elif event["kind"] == EventType.ERROR:
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["kind"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message (id=%s)", event["msg_id"])
|
||||
await process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
@@ -2,44 +2,45 @@
|
||||
"""
|
||||
Example echo bot without using hooks
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||
|
||||
|
||||
def main():
|
||||
with Rpc() as rpc:
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = deltachat.get_system_info()
|
||||
system_info = await deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
|
||||
account.set_config("bot", "1")
|
||||
if not account.is_configured():
|
||||
await account.set_config("bot", "1")
|
||||
if not await account.is_configured():
|
||||
logging.info("Account is not configured, configuring")
|
||||
account.set_config("addr", sys.argv[1])
|
||||
account.set_config("mail_pw", sys.argv[2])
|
||||
account.configure()
|
||||
await account.set_config("addr", sys.argv[1])
|
||||
await account.set_config("mail_pw", sys.argv[2])
|
||||
await account.configure()
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
deltachat.start_io()
|
||||
await deltachat.start_io()
|
||||
|
||||
def process_messages():
|
||||
for message in account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
async def process_messages():
|
||||
for message in await account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.message.mark_seen()
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
# Process old messages.
|
||||
process_messages()
|
||||
await process_messages()
|
||||
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
event = await account.wait_for_event()
|
||||
if event["type"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["type"] == EventType.WARNING:
|
||||
@@ -48,9 +49,9 @@ def main():
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["type"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
process_messages()
|
||||
await process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
main()
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -5,8 +5,12 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
dependencies = [
|
||||
"aiohttp"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: AsyncIO",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
@@ -71,6 +75,3 @@ line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
"""Delta Chat asynchronous high-level API"""
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -43,7 +43,7 @@ class AttrDict(dict):
|
||||
super().__setattr__(attr, val)
|
||||
|
||||
|
||||
def run_client_cli(
|
||||
async def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -54,10 +54,10 @@ def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
_run_cli(Client, hooks, argv, **kwargs)
|
||||
await _run_cli(Client, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def run_bot_cli(
|
||||
async def run_bot_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -68,10 +68,10 @@ def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
_run_cli(Bot, hooks, argv, **kwargs)
|
||||
await _run_cli(Bot, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def _run_cli(
|
||||
async def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
@@ -93,20 +93,20 @@ def _run_cli(
|
||||
parser.add_argument("--password", action="store", help="password")
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
core_version = (deltachat.get_system_info()).deltachat_core_version
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
core_version = (await deltachat.get_system_info()).deltachat_core_version
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
|
||||
client = client_type(account, hooks)
|
||||
client.logger.debug("Running deltachat core %s", core_version)
|
||||
if not client.is_configured():
|
||||
if not await client.is_configured():
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
# Save a reference to avoid garbage collection of the task.
|
||||
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
|
||||
await client.run_forever()
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
|
||||
@@ -24,63 +24,63 @@ class Account:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.manager.rpc
|
||||
|
||||
def wait_for_event(self) -> AttrDict:
|
||||
async def wait_for_event(self) -> AttrDict:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
return AttrDict(await self._rpc.wait_for_event(self.id))
|
||||
|
||||
def remove(self) -> None:
|
||||
async def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
self._rpc.remove_account(self.id)
|
||||
await self._rpc.remove_account(self.id)
|
||||
|
||||
def start_io(self) -> None:
|
||||
async def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
self._rpc.start_io(self.id)
|
||||
await self._rpc.start_io(self.id)
|
||||
|
||||
def stop_io(self) -> None:
|
||||
async def stop_io(self) -> None:
|
||||
"""Stop the account I/O."""
|
||||
self._rpc.stop_io(self.id)
|
||||
await self._rpc.stop_io(self.id)
|
||||
|
||||
def get_info(self) -> AttrDict:
|
||||
async def get_info(self) -> AttrDict:
|
||||
"""Return dictionary of this account configuration parameters."""
|
||||
return AttrDict(self._rpc.get_info(self.id))
|
||||
return AttrDict(await self._rpc.get_info(self.id))
|
||||
|
||||
def get_size(self) -> int:
|
||||
async def get_size(self) -> int:
|
||||
"""Get the combined filesize of an account in bytes."""
|
||||
return self._rpc.get_account_file_size(self.id)
|
||||
return await self._rpc.get_account_file_size(self.id)
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
async def is_configured(self) -> bool:
|
||||
"""Return True if this account is configured."""
|
||||
return self._rpc.is_configured(self.id)
|
||||
return await self._rpc.is_configured(self.id)
|
||||
|
||||
def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
async def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
"""Set configuration value."""
|
||||
self._rpc.set_config(self.id, key, value)
|
||||
await self._rpc.set_config(self.id, key, value)
|
||||
|
||||
def get_config(self, key: str) -> Optional[str]:
|
||||
async def get_config(self, key: str) -> Optional[str]:
|
||||
"""Get configuration value."""
|
||||
return self._rpc.get_config(self.id, key)
|
||||
return await self._rpc.get_config(self.id, key)
|
||||
|
||||
def update_config(self, **kwargs) -> None:
|
||||
async def update_config(self, **kwargs) -> None:
|
||||
"""update config values."""
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, value)
|
||||
await self.set_config(key, value)
|
||||
|
||||
def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||
async def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
Passing None will discard the currently set avatar.
|
||||
"""
|
||||
self.set_config("selfavatar", img_path)
|
||||
await self.set_config("selfavatar", img_path)
|
||||
|
||||
def get_avatar(self) -> Optional[str]:
|
||||
async def get_avatar(self) -> Optional[str]:
|
||||
"""Get self avatar."""
|
||||
return self.get_config("selfavatar")
|
||||
return await self.get_config("selfavatar")
|
||||
|
||||
def configure(self) -> None:
|
||||
async def configure(self) -> None:
|
||||
"""Configure an account."""
|
||||
self._rpc.configure(self.id)
|
||||
await self._rpc.configure(self.id)
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -94,24 +94,24 @@ class Account:
|
||||
if isinstance(obj, int):
|
||||
obj = Contact(self, obj)
|
||||
if isinstance(obj, Contact):
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
obj = (await obj.get_snapshot()).address
|
||||
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
async def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
contacts = await self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
def get_contacts(
|
||||
async def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
@@ -133,9 +133,9 @@ class Account:
|
||||
flags |= ContactFlag.ADD_SELF
|
||||
|
||||
if snapshot:
|
||||
contacts = self._rpc.get_contacts(self.id, flags, query)
|
||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
contacts = self._rpc.get_contact_ids(self.id, flags, query)
|
||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
||||
return [Contact(self, contact_id) for contact_id in contacts]
|
||||
|
||||
@property
|
||||
@@ -143,7 +143,7 @@ class Account:
|
||||
"""This account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
def get_chatlist(
|
||||
async def get_chatlist(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
contact: Optional[Contact] = None,
|
||||
@@ -175,29 +175,29 @@ class Account:
|
||||
if alldone_hint:
|
||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||
|
||||
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
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]
|
||||
|
||||
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
chats = []
|
||||
for item in items.values():
|
||||
item["chat"] = Chat(self, item["id"])
|
||||
chats.append(AttrDict(item))
|
||||
return chats
|
||||
|
||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
async def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
"""Create a new group chat.
|
||||
|
||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||
"""
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
|
||||
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
"""Return the Chat instance with the given ID."""
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
async def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
||||
another device.
|
||||
|
||||
@@ -208,62 +208,62 @@ class Account:
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
"""
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
async def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||
"""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
"""Return the Message instance with the given ID."""
|
||||
return Message(self, msg_id)
|
||||
|
||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
async def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
"""Mark the given set of messages as seen."""
|
||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
async def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""Delete messages (local and remote)."""
|
||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
|
||||
def get_fresh_messages(self) -> List[Message]:
|
||||
async def get_fresh_messages(self) -> List[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||
to process oldest messages first.
|
||||
"""
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def get_next_messages(self) -> List[Message]:
|
||||
async def get_next_messages(self) -> List[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
next_msg_ids = await self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_next_messages(self) -> List[Message]:
|
||||
async def wait_next_messages(self) -> List[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
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(self._rpc.get_fresh_msgs(self.id))
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def export_backup(self, path, passphrase: str = "") -> None:
|
||||
async def export_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Export backup."""
|
||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
await self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
|
||||
def import_backup(self, path, passphrase: str = "") -> None:
|
||||
async def import_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Import backup."""
|
||||
self._rpc.import_backup(self.id, str(path), passphrase)
|
||||
await self._rpc.import_backup(self.id, str(path), passphrase)
|
||||
|
||||
@@ -25,7 +25,7 @@ class Chat:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
def delete(self) -> None:
|
||||
async def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -33,83 +33,83 @@ class Chat:
|
||||
- does not delete messages on server
|
||||
- the chat or contact is not blocked, new message will arrive
|
||||
"""
|
||||
self._rpc.delete_chat(self.account.id, self.id)
|
||||
await self._rpc.delete_chat(self.account.id, self.id)
|
||||
|
||||
def block(self) -> None:
|
||||
async def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
self._rpc.block_chat(self.account.id, self.id)
|
||||
await self._rpc.block_chat(self.account.id, self.id)
|
||||
|
||||
def accept(self) -> None:
|
||||
async def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
self._rpc.accept_chat(self.account.id, self.id)
|
||||
await self._rpc.accept_chat(self.account.id, self.id)
|
||||
|
||||
def leave(self) -> None:
|
||||
async def leave(self) -> None:
|
||||
"""Leave this chat."""
|
||||
self._rpc.leave_group(self.account.id, self.id)
|
||||
await self._rpc.leave_group(self.account.id, self.id)
|
||||
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
async def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
||||
|
||||
:param duration: mute duration from now in seconds. Must be greater than zero.
|
||||
"""
|
||||
if duration is not None:
|
||||
assert duration > 0, "Invalid duration"
|
||||
dur: dict = {"kind": "Until", "duration": duration}
|
||||
dur: Union[str, dict] = {"Until": duration}
|
||||
else:
|
||||
dur = {"kind": "Forever"}
|
||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
dur = "Forever"
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
|
||||
def unmute(self) -> None:
|
||||
async def unmute(self) -> None:
|
||||
"""Unmute this chat."""
|
||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
||||
|
||||
def pin(self) -> None:
|
||||
async def pin(self) -> None:
|
||||
"""Pin this chat."""
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
|
||||
def unpin(self) -> None:
|
||||
async def unpin(self) -> None:
|
||||
"""Unpin this chat."""
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
def archive(self) -> None:
|
||||
async def archive(self) -> None:
|
||||
"""Archive this chat."""
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
|
||||
def unarchive(self) -> None:
|
||||
async def unarchive(self) -> None:
|
||||
"""Unarchive this chat."""
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
def set_name(self, name: str) -> None:
|
||||
async def set_name(self, name: str) -> None:
|
||||
"""Set name of this chat."""
|
||||
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
await self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
async def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat."""
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
async def get_encryption_info(self) -> str:
|
||||
"""Return encryption info for this chat."""
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
async def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
def get_basic_snapshot(self) -> AttrDict:
|
||||
async def get_basic_snapshot(self) -> AttrDict:
|
||||
"""Get a chat snapshot with basic info about this chat."""
|
||||
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
def get_full_snapshot(self) -> AttrDict:
|
||||
async def get_full_snapshot(self) -> AttrDict:
|
||||
"""Get a full snapshot of this chat."""
|
||||
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
def can_send(self) -> bool:
|
||||
async def can_send(self) -> bool:
|
||||
"""Return true if messages can be sent to the chat."""
|
||||
return self._rpc.can_send(self.account.id, self.id)
|
||||
return await self._rpc.can_send(self.account.id, self.id)
|
||||
|
||||
def send_message(
|
||||
async def send_message(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
@@ -132,30 +132,30 @@ class Chat:
|
||||
"overrideSenderName": override_sender_name,
|
||||
"quotedMessageId": quoted_msg,
|
||||
}
|
||||
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_text(self, text: str) -> Message:
|
||||
async def send_text(self, text: str) -> Message:
|
||||
"""Send a text message and return the resulting Message instance."""
|
||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_videochat_invitation(self) -> Message:
|
||||
async def send_videochat_invitation(self) -> Message:
|
||||
"""Send a videochat invitation and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_sticker(self, path: str) -> Message:
|
||||
async def send_sticker(self, path: str) -> Message:
|
||||
"""Send an sticker and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def forward_messages(self, messages: List[Message]) -> None:
|
||||
async def forward_messages(self, messages: List[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
|
||||
def set_draft(
|
||||
async def set_draft(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
file: Optional[str] = None,
|
||||
@@ -164,15 +164,15 @@ class Chat:
|
||||
"""Set draft message."""
|
||||
if isinstance(quoted_msg, Message):
|
||||
quoted_msg = quoted_msg.id
|
||||
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||
|
||||
def remove_draft(self) -> None:
|
||||
async def remove_draft(self) -> None:
|
||||
"""Remove draft message."""
|
||||
self._rpc.remove_draft(self.account.id, self.id)
|
||||
await self._rpc.remove_draft(self.account.id, self.id)
|
||||
|
||||
def get_draft(self) -> Optional[AttrDict]:
|
||||
async def get_draft(self) -> Optional[AttrDict]:
|
||||
"""Get draft message."""
|
||||
snapshot = self._rpc.get_draft(self.account.id, self.id)
|
||||
snapshot = await self._rpc.get_draft(self.account.id, self.id)
|
||||
if not snapshot:
|
||||
return None
|
||||
snapshot = AttrDict(snapshot)
|
||||
@@ -181,61 +181,61 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
def get_fresh_message_count(self) -> int:
|
||||
async def get_fresh_message_count(self) -> int:
|
||||
"""Get number of fresh messages in this chat"""
|
||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
|
||||
def mark_noticed(self) -> None:
|
||||
async def mark_noticed(self) -> None:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
await self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
contact_id = (await self.account.create_contact(cnt)).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
else:
|
||||
contact_id = cnt
|
||||
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
"""Remove members from this group."""
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
contact_id = (await self.account.create_contact(cnt)).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
else:
|
||||
contact_id = cnt
|
||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def get_contacts(self) -> List[Contact]:
|
||||
async def get_contacts(self) -> List[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
"""
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
def set_image(self, path: str) -> None:
|
||||
async def set_image(self, path: str) -> None:
|
||||
"""Set profile image of this chat.
|
||||
|
||||
:param path: Full path of the image to use as the group image.
|
||||
"""
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||
|
||||
def remove_image(self) -> None:
|
||||
async def remove_image(self) -> None:
|
||||
"""Remove profile image of this chat."""
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
|
||||
def get_locations(
|
||||
async def get_locations(
|
||||
self,
|
||||
contact: Optional[Contact] = None,
|
||||
timestamp_from: Optional["datetime"] = None,
|
||||
@@ -246,7 +246,7 @@ class Chat:
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
contact_id = contact.id if contact else 0
|
||||
|
||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
import inspect
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
@@ -76,22 +78,22 @@ class Client:
|
||||
)
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return self.account.is_configured()
|
||||
async def is_configured(self) -> bool:
|
||||
return await self.account.is_configured()
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
self.account.set_config("addr", email)
|
||||
self.account.set_config("mail_pw", password)
|
||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
await self.account.set_config("addr", email)
|
||||
await self.account.set_config("mail_pw", password)
|
||||
for key, value in kwargs.items():
|
||||
self.account.set_config(key, value)
|
||||
self.account.configure()
|
||||
await self.account.set_config(key, value)
|
||||
await self.account.configure()
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
def run_forever(self) -> None:
|
||||
async def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
self.run_until(lambda _: False)
|
||||
await self.run_until(lambda _: False)
|
||||
|
||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
@@ -99,37 +101,39 @@ class Client:
|
||||
evaluates to True.
|
||||
"""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if self.is_configured():
|
||||
self.account.start_io()
|
||||
self._process_messages() # Process old messages.
|
||||
if await self.is_configured():
|
||||
await self.account.start_io()
|
||||
await self._process_messages() # Process old messages.
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
event["kind"] = EventType(event.kind)
|
||||
event = await self.account.wait_for_event()
|
||||
event["type"] = EventType(event.type)
|
||||
event["account"] = self.account
|
||||
self._on_event(event)
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
self._process_messages()
|
||||
await self._on_event(event)
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
await self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
if inspect.isawaitable(stop):
|
||||
stop = await stop
|
||||
if stop:
|
||||
return event
|
||||
|
||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if evfilter.filter(event):
|
||||
if await evfilter.filter(event):
|
||||
try:
|
||||
hook(event)
|
||||
await hook(event)
|
||||
except Exception as ex:
|
||||
self.logger.exception(ex)
|
||||
|
||||
def _parse_command(self, event: AttrDict) -> None:
|
||||
async def _parse_command(self, event: AttrDict) -> None:
|
||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||
payload = parts[1] if len(parts) > 1 else ""
|
||||
cmd = parts.pop(0)
|
||||
|
||||
if "@" in cmd:
|
||||
suffix = "@" + self.account.self_contact.get_snapshot().address
|
||||
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
|
||||
if cmd.endswith(suffix):
|
||||
cmd = cmd[: -len(suffix)]
|
||||
else:
|
||||
@@ -149,32 +153,32 @@ class Client:
|
||||
|
||||
event["command"], event["payload"] = cmd, payload
|
||||
|
||||
def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
async def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
||||
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
||||
self._parse_command(event)
|
||||
self._on_event(event, NewMessage)
|
||||
await self._parse_command(event)
|
||||
await self._on_event(event, NewMessage)
|
||||
|
||||
def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(message_snapshot=snapshot)
|
||||
|
||||
img_changed = parse_system_image_changed(snapshot.text)
|
||||
if img_changed:
|
||||
_, event["image_deleted"] = img_changed
|
||||
self._on_event(event, GroupImageChanged)
|
||||
await self._on_event(event, GroupImageChanged)
|
||||
return
|
||||
|
||||
title_changed = parse_system_title_changed(snapshot.text)
|
||||
if title_changed:
|
||||
_, event["old_name"] = title_changed
|
||||
self._on_event(event, GroupNameChanged)
|
||||
await self._on_event(event, GroupNameChanged)
|
||||
return
|
||||
|
||||
members_changed = parse_system_add_remove(snapshot.text)
|
||||
if members_changed:
|
||||
action, event["member"], _ = members_changed
|
||||
event["member_added"] = action == "added"
|
||||
self._on_event(event, MemberListChanged)
|
||||
await self._on_event(event, MemberListChanged)
|
||||
return
|
||||
|
||||
self.logger.warning(
|
||||
@@ -183,20 +187,20 @@ class Client:
|
||||
snapshot.text,
|
||||
)
|
||||
|
||||
def _process_messages(self) -> None:
|
||||
async def _process_messages(self) -> None:
|
||||
if self._should_process_messages:
|
||||
for message in self.account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
for message in await self.account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||
self._on_new_msg(snapshot)
|
||||
await self._on_new_msg(snapshot)
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
self._handle_info_msg(snapshot)
|
||||
snapshot.message.mark_seen()
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
|
||||
class Bot(Client):
|
||||
"""Simple bot implementation that listent to events of a single account."""
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
kwargs.setdefault("bot", "1")
|
||||
super().configure(email, password, **kwargs)
|
||||
await super().configure(email, password, **kwargs)
|
||||
|
||||
@@ -24,39 +24,39 @@ class Contact:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
def block(self) -> None:
|
||||
async def block(self) -> None:
|
||||
"""Block contact."""
|
||||
self._rpc.block_contact(self.account.id, self.id)
|
||||
await self._rpc.block_contact(self.account.id, self.id)
|
||||
|
||||
def unblock(self) -> None:
|
||||
async def unblock(self) -> None:
|
||||
"""Unblock contact."""
|
||||
self._rpc.unblock_contact(self.account.id, self.id)
|
||||
await self._rpc.unblock_contact(self.account.id, self.id)
|
||||
|
||||
def delete(self) -> None:
|
||||
async def delete(self) -> None:
|
||||
"""Delete contact."""
|
||||
self._rpc.delete_contact(self.account.id, self.id)
|
||||
await self._rpc.delete_contact(self.account.id, self.id)
|
||||
|
||||
def set_name(self, name: str) -> None:
|
||||
async def set_name(self, name: str) -> None:
|
||||
"""Change the name of this contact."""
|
||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
await self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
async def get_encryption_info(self) -> str:
|
||||
"""Get a multi-line encryption info, containing your fingerprint and
|
||||
the fingerprint of the contact.
|
||||
"""
|
||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
async def get_snapshot(self) -> AttrDict:
|
||||
"""Return a dictionary with a snapshot of all contact properties."""
|
||||
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
|
||||
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
|
||||
snapshot["contact"] = self
|
||||
return snapshot
|
||||
|
||||
def create_chat(self) -> "Chat":
|
||||
async def create_chat(self) -> "Chat":
|
||||
"""Create or get an existing 1:1 chat for this contact."""
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
self.account,
|
||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
)
|
||||
|
||||
@@ -16,34 +16,34 @@ class DeltaChat:
|
||||
def __init__(self, rpc: "Rpc") -> None:
|
||||
self.rpc = rpc
|
||||
|
||||
def add_account(self) -> Account:
|
||||
async def add_account(self) -> Account:
|
||||
"""Create a new account database."""
|
||||
account_id = self.rpc.add_account()
|
||||
account_id = await self.rpc.add_account()
|
||||
return Account(self, account_id)
|
||||
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
async def get_all_accounts(self) -> List[Account]:
|
||||
"""Return a list of all available accounts."""
|
||||
account_ids = self.rpc.get_all_account_ids()
|
||||
account_ids = await self.rpc.get_all_account_ids()
|
||||
return [Account(self, account_id) for account_id in account_ids]
|
||||
|
||||
def start_io(self) -> None:
|
||||
async def start_io(self) -> None:
|
||||
"""Start the I/O of all accounts."""
|
||||
self.rpc.start_io_for_all_accounts()
|
||||
await self.rpc.start_io_for_all_accounts()
|
||||
|
||||
def stop_io(self) -> None:
|
||||
async def stop_io(self) -> None:
|
||||
"""Stop the I/O of all accounts."""
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
await self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
async def maybe_network(self) -> None:
|
||||
"""Indicate that the network likely has come back or just that the network
|
||||
conditions might have changed.
|
||||
"""
|
||||
self.rpc.maybe_network()
|
||||
await self.rpc.maybe_network()
|
||||
|
||||
def get_system_info(self) -> AttrDict:
|
||||
async def get_system_info(self) -> AttrDict:
|
||||
"""Get information about the Delta Chat core in this system."""
|
||||
return AttrDict(self.rpc.get_system_info())
|
||||
return AttrDict(await self.rpc.get_system_info())
|
||||
|
||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
async def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
await self.rpc.set_stock_strings(translations)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""High-level classes for event processing and filtering."""
|
||||
import inspect
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
@@ -23,7 +24,7 @@ def _tuple_of(obj, type_: type) -> tuple:
|
||||
class EventFilter(ABC):
|
||||
"""The base event filter.
|
||||
|
||||
:param func: A Callable function that should accept the event as input
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -42,13 +43,16 @@ class EventFilter(ABC):
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def _call_func(self, event) -> bool:
|
||||
async def _call_func(self, event) -> bool:
|
||||
if not self.func:
|
||||
return True
|
||||
return self.func(event)
|
||||
res = self.func(event)
|
||||
if inspect.isawaitable(res):
|
||||
return await res
|
||||
return res
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, event):
|
||||
async def filter(self, event):
|
||||
"""Return True-like value if the event passed the filter and should be
|
||||
used, or False-like value otherwise.
|
||||
"""
|
||||
@@ -58,7 +62,7 @@ class RawEvent(EventFilter):
|
||||
"""Matches raw core events.
|
||||
|
||||
:param types: The types of event to match.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -78,10 +82,10 @@ class RawEvent(EventFilter):
|
||||
return (self.types, self.func) == (other.types, other.func)
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.types and event.kind not in self.types:
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
if self.types and event.type not in self.types:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class NewMessage(EventFilter):
|
||||
@@ -100,7 +104,7 @@ class NewMessage(EventFilter):
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -155,7 +159,7 @@ class NewMessage(EventFilter):
|
||||
)
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||
return False
|
||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||
@@ -164,9 +168,11 @@ class NewMessage(EventFilter):
|
||||
return False
|
||||
if self.pattern:
|
||||
match = self.pattern(event.message_snapshot.text)
|
||||
if inspect.isawaitable(match):
|
||||
match = await match
|
||||
if not match:
|
||||
return False
|
||||
return super()._call_func(event)
|
||||
return await super()._call_func(event)
|
||||
|
||||
|
||||
class MemberListChanged(EventFilter):
|
||||
@@ -178,7 +184,7 @@ class MemberListChanged(EventFilter):
|
||||
:param added: If set to True only match if a member was added, if set to False
|
||||
only match if a member was removed. If omitted both, member additions
|
||||
and removals, will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -195,10 +201,10 @@ class MemberListChanged(EventFilter):
|
||||
return (self.added, self.func) == (other.added, other.func)
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
if self.added is not None and self.added != event.member_added:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class GroupImageChanged(EventFilter):
|
||||
@@ -210,7 +216,7 @@ class GroupImageChanged(EventFilter):
|
||||
:param deleted: If set to True only match if the image was deleted, if set to False
|
||||
only match if a new image was set. If omitted both, image changes and
|
||||
removals, will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -227,10 +233,10 @@ class GroupImageChanged(EventFilter):
|
||||
return (self.deleted, self.func) == (other.deleted, other.func)
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class GroupNameChanged(EventFilter):
|
||||
@@ -239,7 +245,7 @@ class GroupNameChanged(EventFilter):
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param func: A Callable function that should accept the event as input
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -252,8 +258,8 @@ class GroupNameChanged(EventFilter):
|
||||
return self.func == other.func
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
return self._call_func(event)
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class HookCollection:
|
||||
|
||||
@@ -21,39 +21,39 @@ class Message:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
def send_reaction(self, *reaction: str):
|
||||
async def send_reaction(self, *reaction: str):
|
||||
"""Send a reaction to this message."""
|
||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
await self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
async def get_snapshot(self) -> AttrDict:
|
||||
"""Get a snapshot with the properties of this message."""
|
||||
from .chat import Chat
|
||||
|
||||
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
|
||||
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
|
||||
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
||||
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
||||
snapshot["message"] = self
|
||||
return snapshot
|
||||
|
||||
def get_reactions(self) -> Optional[AttrDict]:
|
||||
async def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
if reactions:
|
||||
return AttrDict(reactions)
|
||||
return None
|
||||
|
||||
def mark_seen(self) -> None:
|
||||
async def mark_seen(self) -> None:
|
||||
"""Mark the message as seen."""
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
update = json.dumps(update)
|
||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
async def get_webxdc_info(self) -> dict:
|
||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
@@ -1,67 +1,70 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
import pytest
|
||||
import aiohttp
|
||||
import pytest_asyncio
|
||||
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
def get_temp_credentials() -> dict:
|
||||
async def get_temp_credentials() -> dict:
|
||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
||||
|
||||
request = urllib.request.Request(url, method="POST")
|
||||
with urllib.request.urlopen(request, timeout=60) as f:
|
||||
return json.load(f)
|
||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response:
|
||||
return json.loads(await response.text())
|
||||
|
||||
|
||||
class ACFactory:
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
return self.deltachat.add_account()
|
||||
async def get_unconfigured_account(self) -> Account:
|
||||
return await self.deltachat.add_account()
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
async def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(await self.get_unconfigured_account())
|
||||
|
||||
def new_preconfigured_account(self) -> Account:
|
||||
async def new_preconfigured_account(self) -> Account:
|
||||
"""Make a new account with configuration options set, but configuration not started."""
|
||||
credentials = get_temp_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
account.set_config("addr", credentials["email"])
|
||||
account.set_config("mail_pw", credentials["password"])
|
||||
assert not account.is_configured()
|
||||
credentials = await get_temp_credentials()
|
||||
account = await self.get_unconfigured_account()
|
||||
await account.set_config("addr", credentials["email"])
|
||||
await account.set_config("mail_pw", credentials["password"])
|
||||
assert not await account.is_configured()
|
||||
return account
|
||||
|
||||
def new_configured_account(self) -> Account:
|
||||
account = self.new_preconfigured_account()
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
async def new_configured_account(self) -> Account:
|
||||
account = await self.new_preconfigured_account()
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
return account
|
||||
|
||||
def new_configured_bot(self) -> Bot:
|
||||
credentials = get_temp_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
async def new_configured_bot(self) -> Bot:
|
||||
credentials = await get_temp_credentials()
|
||||
bot = await self.get_unconfigured_bot()
|
||||
await bot.configure(credentials["email"], credentials["password"])
|
||||
return bot
|
||||
|
||||
def get_online_account(self) -> Account:
|
||||
account = self.new_configured_account()
|
||||
account.start_io()
|
||||
async def get_online_account(self) -> Account:
|
||||
account = await self.new_configured_account()
|
||||
await account.start_io()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
event = await account.wait_for_event()
|
||||
print(event)
|
||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
return account
|
||||
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
|
||||
|
||||
def send_message(
|
||||
async def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
from_account: Optional[Account] = None,
|
||||
@@ -70,16 +73,16 @@ class ACFactory:
|
||||
group: Optional[str] = None,
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (self.get_online_accounts(1))[0]
|
||||
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
||||
from_account = (await self.get_online_accounts(1))[0]
|
||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
||||
if group:
|
||||
to_chat = from_account.create_group(group)
|
||||
to_chat.add_contact(to_contact)
|
||||
to_chat = await from_account.create_group(group)
|
||||
await to_chat.add_contact(to_contact)
|
||||
else:
|
||||
to_chat = to_contact.create_chat()
|
||||
return to_chat.send_message(text=text, file=file)
|
||||
to_chat = await to_contact.create_chat()
|
||||
return await to_chat.send_message(text=text, file=file)
|
||||
|
||||
def process_message(
|
||||
async def process_message(
|
||||
self,
|
||||
to_client: Client,
|
||||
from_account: Optional[Account] = None,
|
||||
@@ -87,7 +90,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> AttrDict:
|
||||
self.send_message(
|
||||
await self.send_message(
|
||||
to_account=to_client.account,
|
||||
from_account=from_account,
|
||||
text=text,
|
||||
@@ -95,16 +98,16 @@ class ACFactory:
|
||||
group=group,
|
||||
)
|
||||
|
||||
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
@pytest_asyncio.fixture
|
||||
async def rpc(tmp_path) -> AsyncGenerator:
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
with rpc_server:
|
||||
async with rpc_server:
|
||||
yield rpc_server
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
@pytest_asyncio.fixture
|
||||
async def acfactory(rpc) -> AsyncGenerator:
|
||||
yield ACFactory(DeltaChat(rpc))
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@@ -15,7 +10,7 @@ class JsonRpcError(Exception):
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
**kwargs.get("env", os.environ),
|
||||
@@ -23,138 +18,78 @@ class Rpc:
|
||||
}
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.process: subprocess.Popen
|
||||
self.process: asyncio.subprocess.Process
|
||||
self.id: int
|
||||
self.event_queues: Dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: Dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
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_thread: Thread
|
||||
self.writer_thread: Thread
|
||||
self.events_thread: Thread
|
||||
self.reader_task: asyncio.Task
|
||||
self.events_task: asyncio.Task
|
||||
|
||||
def get_async_rpc(self) -> "AsyncRpc":
|
||||
"""Get asynchronous wrapper to use the RPC methods from async code."""
|
||||
return AsyncRpc(self)
|
||||
|
||||
def start(self) -> None:
|
||||
if sys.version_info >= (3, 11):
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
process_group=0,
|
||||
**self._kwargs,
|
||||
)
|
||||
else:
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
async def start(self) -> None:
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
self.request_queue = Queue()
|
||||
self.closing = False
|
||||
self.reader_thread = Thread(target=self.reader_loop)
|
||||
self.reader_thread.start()
|
||||
self.writer_thread = Thread(target=self.writer_loop)
|
||||
self.writer_thread.start()
|
||||
self.events_thread = Thread(target=self.events_loop)
|
||||
self.events_thread.start()
|
||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
||||
self.events_task = asyncio.create_task(self.events_loop())
|
||||
|
||||
def close(self) -> None:
|
||||
async def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
self.stop_io_for_all_accounts()
|
||||
self.events_thread.join()
|
||||
self.process.stdin.close()
|
||||
self.reader_thread.join()
|
||||
self.request_queue.put(None)
|
||||
self.writer_thread.join()
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc, _tb):
|
||||
self.close()
|
||||
await self.stop_io_for_all_accounts()
|
||||
await self.events_task
|
||||
self.process.terminate()
|
||||
await self.reader_task
|
||||
|
||||
async def __aenter__(self):
|
||||
self.__enter__()
|
||||
return self.get_async_rpc()
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
||||
self.__exit__(_exc_type, _exc, _tb)
|
||||
await self.close()
|
||||
|
||||
def reader_loop(self) -> None:
|
||||
try:
|
||||
while True:
|
||||
line = self.process.stdout.readline()
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
response_id = response["id"]
|
||||
event = self.request_events.pop(response_id)
|
||||
self.request_results[response_id] = response
|
||||
event.set()
|
||||
else:
|
||||
logging.warning("Got a response without ID: %s", response)
|
||||
except Exception:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
async def reader_loop(self) -> None:
|
||||
while True:
|
||||
line = await self.process.stdout.readline() # noqa
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
else:
|
||||
print(response)
|
||||
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
try:
|
||||
while True:
|
||||
request = self.request_queue.get()
|
||||
if not request:
|
||||
break
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
|
||||
except Exception:
|
||||
# Log an exception if the writer loop dies.
|
||||
logging.exception("Exception in the writer loop")
|
||||
|
||||
def get_queue(self, account_id: int) -> Queue:
|
||||
async def get_queue(self, account_id: int) -> asyncio.Queue:
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = Queue()
|
||||
self.event_queues[account_id] = asyncio.Queue()
|
||||
return self.event_queues[account_id]
|
||||
|
||||
def events_loop(self) -> None:
|
||||
async def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
try:
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
queue.put(event["event"])
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
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"])
|
||||
|
||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
queue = await self.get_queue(account_id)
|
||||
return await queue.get()
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
def method(*args) -> Any:
|
||||
async def method(*args) -> Any:
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
@@ -164,12 +99,12 @@ class Rpc:
|
||||
"params": args,
|
||||
"id": self.id,
|
||||
}
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
self.request_queue.put(request)
|
||||
event.wait()
|
||||
|
||||
response = self.request_results.pop(request_id)
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data) # noqa
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
self.request_events[request_id] = fut
|
||||
response = await fut
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
@@ -177,19 +112,3 @@ class Rpc:
|
||||
return None
|
||||
|
||||
return method
|
||||
|
||||
|
||||
class AsyncRpc:
|
||||
def __init__(self, sync_rpc: Rpc) -> None:
|
||||
self._sync_rpc = sync_rpc
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
sync_method = getattr(self._sync_rpc, attr)
|
||||
if sync_method:
|
||||
|
||||
async def method(*args) -> Any:
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, sync_method, *args)
|
||||
|
||||
return method
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
import subprocess
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -8,26 +6,26 @@ from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_system_info(rpc) -> None:
|
||||
system_info = rpc.get_system_info()
|
||||
@pytest.mark.asyncio()
|
||||
async def test_system_info(rpc) -> None:
|
||||
system_info = await rpc.get_system_info()
|
||||
assert "arch" in system_info
|
||||
assert "deltachat_core_version" in system_info
|
||||
|
||||
|
||||
def test_sleep(rpc) -> None:
|
||||
@pytest.mark.asyncio()
|
||||
async def test_sleep(rpc) -> None:
|
||||
"""Test that long-running task does not block short-running task from completion."""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
sleep_5_future = executor.submit(rpc.sleep, 5.0)
|
||||
sleep_3_future = executor.submit(rpc.sleep, 3.0)
|
||||
done, pending = concurrent.futures.wait(
|
||||
[sleep_5_future, sleep_3_future],
|
||||
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||
)
|
||||
assert sleep_3_future in done
|
||||
assert sleep_5_future in pending
|
||||
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
|
||||
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
|
||||
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
|
||||
assert sleep_3_task in done
|
||||
assert sleep_5_task in pending
|
||||
sleep_5_task.cancel()
|
||||
|
||||
|
||||
def test_email_address_validity(rpc) -> None:
|
||||
@pytest.mark.asyncio()
|
||||
async def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||
@@ -35,16 +33,17 @@ def test_email_address_validity(rpc) -> None:
|
||||
invalid_addresses = ["email@", "example.com", "emai221"]
|
||||
|
||||
for addr in valid_addresses:
|
||||
assert rpc.check_email_validity(addr)
|
||||
assert await rpc.check_email_validity(addr)
|
||||
for addr in invalid_addresses:
|
||||
assert not rpc.check_email_validity(addr)
|
||||
assert not await rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
def test_acfactory(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
@pytest.mark.asyncio()
|
||||
async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||
event = await account.wait_for_event()
|
||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
||||
assert event.progress != 0 # Progress 0 indicates error.
|
||||
if event.progress == 1000: # Success
|
||||
break
|
||||
@@ -53,241 +52,248 @@ def test_acfactory(acfactory) -> None:
|
||||
print("Successful configuration")
|
||||
|
||||
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
@pytest.mark.asyncio()
|
||||
async def test_configure_starttls(acfactory) -> None:
|
||||
account = await acfactory.new_preconfigured_account()
|
||||
|
||||
# Use STARTTLS
|
||||
account.set_config("mail_security", "2")
|
||||
account.set_config("send_security", "2")
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
await account.set_config("mail_security", "2")
|
||||
await account.set_config("send_security", "2")
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_account(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = await message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
bob.mark_seen_messages([message])
|
||||
await bob.mark_seen_messages([message])
|
||||
|
||||
assert alice != bob
|
||||
assert repr(alice)
|
||||
assert alice.get_info().level
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert (await alice.get_info()).level
|
||||
assert await alice.get_size()
|
||||
assert await alice.is_configured()
|
||||
assert not await alice.get_avatar()
|
||||
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert await alice.get_contacts()
|
||||
assert await alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
assert alice.get_chatlist()
|
||||
assert alice.get_chatlist(snapshot=True)
|
||||
assert alice.get_qr_code()
|
||||
assert alice.get_fresh_messages()
|
||||
assert alice.get_next_messages()
|
||||
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()
|
||||
|
||||
# Test sending empty message.
|
||||
assert len(bob.wait_next_messages()) == 0
|
||||
alice_chat_bob.send_text("")
|
||||
messages = bob.wait_next_messages()
|
||||
assert len(await bob.wait_next_messages()) == 0
|
||||
await alice_chat_bob.send_text("")
|
||||
messages = await bob.wait_next_messages()
|
||||
assert len(messages) == 1
|
||||
message = messages[0]
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = await message.get_snapshot()
|
||||
assert snapshot.text == ""
|
||||
bob.mark_seen_messages([message])
|
||||
await bob.mark_seen_messages([message])
|
||||
|
||||
group = alice.create_group("test group")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group_msg = group.send_message(text="hello")
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
group_msg = await group.send_message(text="hello")
|
||||
assert group_msg == alice.get_message_by_id(group_msg.id)
|
||||
assert group == alice.get_chat_by_id(group.id)
|
||||
alice.delete_messages([group_msg])
|
||||
await alice.delete_messages([group_msg])
|
||||
|
||||
alice.set_config("selfstatus", "test")
|
||||
assert alice.get_config("selfstatus") == "test"
|
||||
alice.update_config(selfstatus="test2")
|
||||
assert alice.get_config("selfstatus") == "test2"
|
||||
await alice.set_config("selfstatus", "test")
|
||||
assert await alice.get_config("selfstatus") == "test"
|
||||
await alice.update_config(selfstatus="test2")
|
||||
assert await alice.get_config("selfstatus") == "test2"
|
||||
|
||||
assert not alice.get_blocked_contacts()
|
||||
alice_contact_bob.block()
|
||||
blocked_contacts = alice.get_blocked_contacts()
|
||||
assert not await alice.get_blocked_contacts()
|
||||
await alice_contact_bob.block()
|
||||
blocked_contacts = await alice.get_blocked_contacts()
|
||||
assert blocked_contacts
|
||||
assert blocked_contacts[0].contact == alice_contact_bob
|
||||
|
||||
bob.remove()
|
||||
alice.stop_io()
|
||||
await bob.remove()
|
||||
await alice.stop_io()
|
||||
|
||||
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_chat(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = await message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
||||
|
||||
assert alice_chat_bob != bob_chat_alice
|
||||
assert repr(alice_chat_bob)
|
||||
alice_chat_bob.delete()
|
||||
assert not bob_chat_alice.can_send()
|
||||
bob_chat_alice.accept()
|
||||
assert bob_chat_alice.can_send()
|
||||
bob_chat_alice.block()
|
||||
bob_chat_alice = snapshot.sender.create_chat()
|
||||
bob_chat_alice.mute()
|
||||
bob_chat_alice.unmute()
|
||||
bob_chat_alice.pin()
|
||||
bob_chat_alice.unpin()
|
||||
bob_chat_alice.archive()
|
||||
bob_chat_alice.unarchive()
|
||||
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()
|
||||
await bob_chat_alice.unmute()
|
||||
await bob_chat_alice.pin()
|
||||
await bob_chat_alice.unpin()
|
||||
await bob_chat_alice.archive()
|
||||
await bob_chat_alice.unarchive()
|
||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||
bob_chat_alice.set_name("test")
|
||||
bob_chat_alice.set_ephemeral_timer(300)
|
||||
bob_chat_alice.get_encryption_info()
|
||||
await bob_chat_alice.set_name("test")
|
||||
await bob_chat_alice.set_ephemeral_timer(300)
|
||||
await bob_chat_alice.get_encryption_info()
|
||||
|
||||
group = alice.create_group("test group")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.get_qr_code()
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
await group.get_qr_code()
|
||||
|
||||
snapshot = group.get_basic_snapshot()
|
||||
snapshot = await group.get_basic_snapshot()
|
||||
assert snapshot.name == "test group"
|
||||
group.set_name("new name")
|
||||
snapshot = group.get_full_snapshot()
|
||||
await group.set_name("new name")
|
||||
snapshot = await group.get_full_snapshot()
|
||||
assert snapshot.name == "new name"
|
||||
|
||||
msg = group.send_message(text="hi")
|
||||
assert (msg.get_snapshot()).text == "hi"
|
||||
group.forward_messages([msg])
|
||||
msg = await group.send_message(text="hi")
|
||||
assert (await msg.get_snapshot()).text == "hi"
|
||||
await group.forward_messages([msg])
|
||||
|
||||
group.set_draft(text="test draft")
|
||||
draft = group.get_draft()
|
||||
await group.set_draft(text="test draft")
|
||||
draft = await group.get_draft()
|
||||
assert draft.text == "test draft"
|
||||
group.remove_draft()
|
||||
assert not group.get_draft()
|
||||
await group.remove_draft()
|
||||
assert not await group.get_draft()
|
||||
|
||||
assert group.get_messages()
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_contacts()
|
||||
group.remove_contact(alice_chat_bob)
|
||||
group.get_locations()
|
||||
assert await group.get_messages()
|
||||
await group.get_fresh_message_count()
|
||||
await group.mark_noticed()
|
||||
assert await group.get_contacts()
|
||||
await group.remove_contact(alice_chat_bob)
|
||||
await group.get_locations()
|
||||
|
||||
|
||||
def test_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_contact(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
assert repr(alice_contact_bob)
|
||||
alice_contact_bob.block()
|
||||
alice_contact_bob.unblock()
|
||||
alice_contact_bob.set_name("new name")
|
||||
alice_contact_bob.get_encryption_info()
|
||||
snapshot = alice_contact_bob.get_snapshot()
|
||||
await alice_contact_bob.block()
|
||||
await alice_contact_bob.unblock()
|
||||
await alice_contact_bob.set_name("new name")
|
||||
await alice_contact_bob.get_encryption_info()
|
||||
snapshot = await alice_contact_bob.get_snapshot()
|
||||
assert snapshot.address == bob_addr
|
||||
alice_contact_bob.create_chat()
|
||||
await alice_contact_bob.create_chat()
|
||||
|
||||
|
||||
def test_message(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_message(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = await message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert not snapshot.is_bot
|
||||
assert repr(message)
|
||||
|
||||
with pytest.raises(JsonRpcError): # chat is not accepted
|
||||
snapshot.chat.send_text("hi")
|
||||
snapshot.chat.accept()
|
||||
snapshot.chat.send_text("hi")
|
||||
await snapshot.chat.send_text("hi")
|
||||
await snapshot.chat.accept()
|
||||
await snapshot.chat.send_text("hi")
|
||||
|
||||
message.mark_seen()
|
||||
message.send_reaction("😎")
|
||||
reactions = message.get_reactions()
|
||||
await message.mark_seen()
|
||||
await message.send_reaction("😎")
|
||||
reactions = await message.get_reactions()
|
||||
assert reactions
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = await message.get_snapshot()
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_is_bot(acfactory) -> None:
|
||||
@pytest.mark.asyncio()
|
||||
async def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
|
||||
# Alice becomes a bot.
|
||||
alice.set_config("bot", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
await alice.set_config("bot", "1")
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = await message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
break
|
||||
|
||||
|
||||
def test_bot(acfactory) -> None:
|
||||
@pytest.mark.asyncio()
|
||||
async def test_bot(acfactory) -> None:
|
||||
mock = MagicMock()
|
||||
user = (acfactory.get_online_accounts(1))[0]
|
||||
bot = acfactory.new_configured_bot()
|
||||
bot2 = acfactory.new_configured_bot()
|
||||
user = (await acfactory.get_online_accounts(1))[0]
|
||||
bot = await acfactory.new_configured_bot()
|
||||
bot2 = await acfactory.new_configured_bot()
|
||||
|
||||
assert bot.is_configured()
|
||||
assert bot.account.get_config("bot") == "1"
|
||||
assert await bot.is_configured()
|
||||
assert await bot.account.get_config("bot") == "1"
|
||||
|
||||
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
||||
bot.add_hook(*hook)
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert not snapshot.is_bot
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
bot.remove_hook(*hook)
|
||||
@@ -299,62 +305,53 @@ def test_bot(acfactory) -> None:
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
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)
|
||||
|
||||
|
||||
def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
@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 = acfactory.new_preconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.configure()
|
||||
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 bot.wait_next_messages()
|
||||
assert not await bot.wait_next_messages()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = asyncio.create_task(bot.wait_next_messages())
|
||||
|
||||
bot_addr = bot.get_config("addr")
|
||||
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
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 = next_messages_task.result()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
next_messages = await next_messages_task
|
||||
assert len(next_messages) == 1
|
||||
snapshot = await next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_import_export(acfactory, tmp_path) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
alice.export_backup(tmp_path)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_import_export(acfactory, tmp_path) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
await alice.export_backup(tmp_path)
|
||||
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
assert alice2.manager.get_system_info()
|
||||
|
||||
|
||||
def test_openrpc_command_line() -> None:
|
||||
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
|
||||
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
|
||||
openrpc = json.loads(out)
|
||||
assert "openrpc" in openrpc
|
||||
assert "methods" in openrpc
|
||||
alice2 = await acfactory.get_unconfigured_account()
|
||||
await alice2.import_backup(files[0])
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import pytest
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_webxdc(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
break
|
||||
|
||||
webxdc_info = message.get_webxdc_info()
|
||||
webxdc_info = await message.get_webxdc_info()
|
||||
assert webxdc_info == {
|
||||
"document": None,
|
||||
"icon": "icon.png",
|
||||
@@ -26,20 +28,20 @@ def test_webxdc(acfactory) -> None:
|
||||
"summary": None,
|
||||
}
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
status_updates = await message.get_webxdc_status_updates()
|
||||
assert status_updates == []
|
||||
|
||||
bob_chat_alice.accept()
|
||||
message.send_webxdc_status_update({"payload": 42}, "")
|
||||
message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||
await bob_chat_alice.accept()
|
||||
await message.send_webxdc_status_update({"payload": 42}, "")
|
||||
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
status_updates = await message.get_webxdc_status_updates()
|
||||
assert status_updates == [
|
||||
{"payload": 42, "serial": 1, "max_serial": 2},
|
||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||
]
|
||||
|
||||
status_updates = message.get_webxdc_status_updates(1)
|
||||
status_updates = await message.get_webxdc_status_updates(1)
|
||||
assert status_updates == [
|
||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 {posargs}
|
||||
pytest {posargs}
|
||||
setenv =
|
||||
# Avoid stack overflow when Rust core is built without optimizations.
|
||||
RUST_MIN_STACK=8388608
|
||||
@@ -14,8 +14,10 @@ passenv =
|
||||
DCC_NEW_TMP_EMAIL
|
||||
deps =
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
aiohttp
|
||||
aiodns
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.127.0"
|
||||
version = "1.122.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -15,13 +15,13 @@ deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "2.0.0"
|
||||
futures-lite = "1.13.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.105"
|
||||
serde_json = "1.0.99"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.33.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
tokio = { version = "1.29.1", features = ["io-std"] }
|
||||
tokio-util = "0.7.8"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -32,6 +32,3 @@ languages other than Rust, for example:
|
||||
|
||||
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
||||
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
||||
|
||||
Run `deltachat-rpc-server --version` to check the version of the server.
|
||||
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.
|
||||
|
||||
@@ -10,7 +10,6 @@ use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use yerpc::RpcServer as _;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use tokio::signal::unix as signal_unix;
|
||||
@@ -40,12 +39,6 @@ async fn main_impl() -> Result<()> {
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
return Ok(());
|
||||
} else if first_arg.to_str() == Some("--openrpc") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
}
|
||||
println!("{}", CommandApi::openrpc_specification()?);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||
}
|
||||
@@ -63,8 +56,7 @@ async fn main_impl() -> Result<()> {
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await?;
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let accounts = Arc::new(RwLock::new(accounts));
|
||||
|
||||
14
deny.toml
14
deny.toml
@@ -11,6 +11,7 @@ ignore = [
|
||||
# when upgrading.
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "ahash", version = "0.7.6" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
@@ -26,7 +27,10 @@ skip = [
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "hashbrown", version = "<0.14.0" },
|
||||
{ name = "idna", version = "<0.3" },
|
||||
{ name = "indexmap", version = "<2.0.0" },
|
||||
{ name = "linux-raw-sys", version = "0.3.8" },
|
||||
{ name = "num-derive", version = "0.3.3" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
@@ -34,9 +38,8 @@ skip = [
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "redox_syscall", version = "0.2.16" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "ring", version = "0.16.20" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rustix", version = "0.37.21" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
@@ -45,14 +48,18 @@ skip = [
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ 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", version = "0.32.0" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.48" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.48" },
|
||||
{ name = "winreg", version = "0.10.1" },
|
||||
]
|
||||
|
||||
|
||||
@@ -84,4 +91,5 @@ license-files = [
|
||||
github = [
|
||||
"async-email",
|
||||
"deltachat",
|
||||
"quinn-rs",
|
||||
]
|
||||
|
||||
@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
|
||||
#### Upsides:
|
||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
|
||||
|
||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||
|
||||
|
||||
100
examples/simple.rs
Normal file
100
examples/simple.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
EventType::ConfigureProgress { progress, .. } => {
|
||||
log::info!("progress: {}", progress);
|
||||
}
|
||||
EventType::Info(msg) => {
|
||||
log::info!("{}", msg);
|
||||
}
|
||||
EventType::Warning(msg) => {
|
||||
log::warn!("{}", msg);
|
||||
}
|
||||
EventType::Error(msg) => {
|
||||
log::error!("{}", msg);
|
||||
}
|
||||
event => {
|
||||
log::info!("{:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
log::info!("info: {:#?}", info);
|
||||
|
||||
let events = ctx.get_event_emitter();
|
||||
let events_spawn = tokio::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
cb(event.typ);
|
||||
}
|
||||
});
|
||||
|
||||
log::info!("configuring");
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
assert_eq!(args.len(), 3, "requires email password");
|
||||
let email = args[1].clone();
|
||||
let pw = args[2].clone();
|
||||
ctx.set_config(config::Config::Addr, Some(&email))
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(config::Config::MailPw, Some(&pw))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.configure().await.unwrap();
|
||||
|
||||
log::info!("------ RUN ------");
|
||||
ctx.start_io().await;
|
||||
log::info!("--- SENDING A MESSAGE ---");
|
||||
|
||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
|
||||
|
||||
for i in 0..1 {
|
||||
log::info!("sending message {}", i);
|
||||
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// wait for the message to be sent out
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
log::info!("fetching chats..");
|
||||
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
||||
|
||||
for i in 0..chats.len() {
|
||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
log::info!("[{}] msg: {:?}", i, msg);
|
||||
}
|
||||
|
||||
log::info!("stopping");
|
||||
ctx.stop_io().await;
|
||||
log::info!("closing");
|
||||
drop(ctx);
|
||||
events_spawn.await.unwrap();
|
||||
}
|
||||
372
fuzz/Cargo.lock
generated
372
fuzz/Cargo.lock
generated
@@ -177,13 +177,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.9.1"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
|
||||
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
"byte-pool",
|
||||
"chrono",
|
||||
"futures",
|
||||
"imap-proto",
|
||||
@@ -337,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.0"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
@@ -529,6 +529,16 @@ version = "3.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||
|
||||
[[package]]
|
||||
name = "byte-pool"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.12.3"
|
||||
@@ -731,6 +741,16 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.14"
|
||||
@@ -906,7 +926,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.123.0"
|
||||
version = "1.117.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -923,7 +943,6 @@ dependencies = [
|
||||
"encoded-words",
|
||||
"escaper",
|
||||
"fast-socks5",
|
||||
"fd-lock",
|
||||
"format-flowed",
|
||||
"futures",
|
||||
"futures-lite",
|
||||
@@ -936,7 +955,7 @@ dependencies = [
|
||||
"libc",
|
||||
"mailparse 0.14.0",
|
||||
"mime",
|
||||
"num-derive 0.4.0",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
@@ -1410,14 +1429,14 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
|
||||
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1445,17 +1464,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
@@ -1524,17 +1532,6 @@ dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "3.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix 0.38.14",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.12.1"
|
||||
@@ -1619,9 +1616,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
|
||||
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
@@ -1846,15 +1843,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.3"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
@@ -1951,7 +1951,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.7",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -2012,9 +2012,20 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
@@ -2022,9 +2033,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.7"
|
||||
version = "0.24.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
|
||||
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
@@ -2089,10 +2100,10 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
|
||||
dependencies = [
|
||||
"socket2 0.4.7",
|
||||
"socket2",
|
||||
"widestring",
|
||||
"winapi",
|
||||
"winreg 0.10.1",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2219,9 +2230,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
version = "0.2.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2268,12 +2279,6 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.9"
|
||||
@@ -2336,9 +2341,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.5"
|
||||
@@ -2356,9 +2367,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.3"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
@@ -2383,13 +2394,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.8"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2543,17 +2555,6 @@ dependencies = [
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
@@ -2598,9 +2599,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
@@ -2853,7 +2854,7 @@ dependencies = [
|
||||
"md-5",
|
||||
"nom",
|
||||
"num-bigint-dig",
|
||||
"num-derive 0.3.3",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"p256 0.13.1",
|
||||
"p384 0.13.0",
|
||||
@@ -2893,9 +2894,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@@ -3086,9 +3087,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.30.0"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
||||
checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -3113,9 +3114,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.9.5"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989"
|
||||
checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
@@ -3138,7 +3139,7 @@ checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"quinn-proto",
|
||||
"socket2 0.4.7",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
@@ -3267,14 +3268,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.5"
|
||||
version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
||||
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.3.8",
|
||||
"regex-syntax 0.7.5",
|
||||
"regex-syntax 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3286,17 +3286,6 @@ dependencies = [
|
||||
"regex-syntax 0.6.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.28"
|
||||
@@ -3305,15 +3294,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.20"
|
||||
version = "0.11.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
|
||||
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
@@ -3343,7 +3332,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3449,7 +3438,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.0.2",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@@ -3500,26 +3489,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno 0.2.8",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys 0.1.4",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"errno 0.3.3",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.7",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.8"
|
||||
@@ -3581,9 +3557,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.5.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603"
|
||||
checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
@@ -3879,16 +3855,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
@@ -3953,6 +3919,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "stop-token"
|
||||
version = "0.7.0"
|
||||
@@ -3973,21 +3945,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.2"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
|
||||
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.15",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4066,7 +4038,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall",
|
||||
"rustix 0.36.7",
|
||||
"rustix",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
@@ -4175,21 +4147,22 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.4",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4204,13 +4177,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4266,9 +4239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.9"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
|
||||
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -4392,9 +4365,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-proto"
|
||||
version = "0.23.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69"
|
||||
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
@@ -4403,9 +4376,9 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna",
|
||||
"idna 0.2.3",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"lazy_static",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@@ -4417,17 +4390,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-resolver"
|
||||
version = "0.23.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f"
|
||||
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"ipconfig",
|
||||
"lazy_static",
|
||||
"lru-cache",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@@ -4459,9 +4431,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -4508,12 +4480,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.4.1"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.3.0",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
@@ -4676,9 +4648,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.2"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
@@ -4759,51 +4731,21 @@ version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.0",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc 0.42.0",
|
||||
"windows_i686_gnu 0.42.0",
|
||||
"windows_i686_msvc 0.42.0",
|
||||
"windows_x86_64_gnu 0.42.0",
|
||||
"windows_x86_64_gnullvm 0.42.0",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -4822,12 +4764,6 @@ version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.32.0"
|
||||
@@ -4846,12 +4782,6 @@ version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -4870,12 +4800,6 @@ version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.32.0"
|
||||
@@ -4894,24 +4818,12 @@ version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -4930,12 +4842,6 @@ version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
@@ -4945,16 +4851,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x25519-dalek"
|
||||
version = "2.0.0-pre.1"
|
||||
|
||||
@@ -159,8 +159,6 @@ module.exports = {
|
||||
DC_STR_BROADCAST_LIST: 115,
|
||||
DC_STR_CANNOT_LOGIN: 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY: 29,
|
||||
DC_STR_CHAT_PROTECTION_DISABLED: 171,
|
||||
DC_STR_CHAT_PROTECTION_ENABLED: 170,
|
||||
DC_STR_CONFIGURATION_FAILED: 84,
|
||||
DC_STR_CONNECTED: 107,
|
||||
DC_STR_CONNTECTING: 108,
|
||||
@@ -246,6 +244,12 @@ module.exports = {
|
||||
DC_STR_OUTGOING_MESSAGES: 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_PROTECTION_DISABLED: 89,
|
||||
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
|
||||
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
|
||||
DC_STR_PROTECTION_ENABLED: 88,
|
||||
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
|
||||
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
|
||||
@@ -159,8 +159,6 @@ export enum C {
|
||||
DC_STR_BROADCAST_LIST = 115,
|
||||
DC_STR_CANNOT_LOGIN = 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||
DC_STR_CHAT_PROTECTION_DISABLED = 171,
|
||||
DC_STR_CHAT_PROTECTION_ENABLED = 170,
|
||||
DC_STR_CONFIGURATION_FAILED = 84,
|
||||
DC_STR_CONNECTED = 107,
|
||||
DC_STR_CONNTECTING = 108,
|
||||
@@ -246,6 +244,12 @@ export enum C {
|
||||
DC_STR_OUTGOING_MESSAGES = 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_PROTECTION_DISABLED = 89,
|
||||
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
|
||||
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
|
||||
DC_STR_PROTECTION_ENABLED = 88,
|
||||
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
|
||||
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
|
||||
@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens a standalone context (without an account manager)
|
||||
/** Opens a stanalone context (without an account manager)
|
||||
* automatically starts the event handler */
|
||||
static open(cwd: string): Context {
|
||||
const dbFile = join(cwd, 'db.sqlite')
|
||||
@@ -699,6 +699,23 @@ export class Context extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatId
|
||||
* @param protect
|
||||
* @returns success boolean
|
||||
*/
|
||||
setChatProtection(chatId: number, protect: boolean) {
|
||||
debug(`setChatProtection ${chatId} ${protect}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_protection(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
protect ? 1 : 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getChatEphemeralTimer(chatId: number): number {
|
||||
debug(`getChatEphemeralTimer ${chatId}`)
|
||||
return binding.dcn_get_chat_ephemeral_timer(
|
||||
|
||||
@@ -21,15 +21,12 @@ export class AccountManager extends EventEmitter {
|
||||
accountDir: string
|
||||
jsonRpcStarted = false
|
||||
|
||||
constructor(cwd: string, writable = true) {
|
||||
constructor(cwd: string, os = 'deltachat-node') {
|
||||
super()
|
||||
debug('DeltaChat constructor')
|
||||
|
||||
this.accountDir = cwd
|
||||
this.dcn_accounts = binding.dcn_accounts_new(
|
||||
this.accountDir,
|
||||
writable ? 1 : 0
|
||||
)
|
||||
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
|
||||
}
|
||||
|
||||
getAllAccountIds() {
|
||||
|
||||
@@ -1399,6 +1399,18 @@ NAPI_METHOD(dcn_set_chat_name) {
|
||||
NAPI_RETURN_INT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_set_chat_protection) {
|
||||
NAPI_ARGV(3);
|
||||
NAPI_DCN_CONTEXT();
|
||||
NAPI_ARGV_UINT32(chat_id, 1);
|
||||
NAPI_ARGV_INT32(protect, 1);
|
||||
|
||||
int result = dc_set_chat_protection(dcn_context->dc_context,
|
||||
chat_id,
|
||||
protect);
|
||||
NAPI_RETURN_INT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
|
||||
NAPI_ARGV(2);
|
||||
NAPI_DCN_CONTEXT();
|
||||
@@ -2903,8 +2915,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
|
||||
|
||||
NAPI_METHOD(dcn_accounts_new) {
|
||||
NAPI_ARGV(2);
|
||||
NAPI_ARGV_UTF8_MALLOC(dir, 0);
|
||||
NAPI_ARGV_INT32(writable, 1);
|
||||
NAPI_ARGV_UTF8_MALLOC(os_name, 0);
|
||||
NAPI_ARGV_UTF8_MALLOC(dir, 1);
|
||||
TRACE("calling..");
|
||||
|
||||
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
|
||||
@@ -2913,7 +2925,7 @@ NAPI_METHOD(dcn_accounts_new) {
|
||||
}
|
||||
|
||||
|
||||
dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
|
||||
dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
|
||||
|
||||
napi_value result;
|
||||
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
|
||||
@@ -3479,6 +3491,7 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_send_msg);
|
||||
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
|
||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
|
||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
|
||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
|
||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);
|
||||
|
||||
@@ -270,7 +270,7 @@ describe('Basic offline Tests', function () {
|
||||
'quota_exceeding',
|
||||
'scan_all_folders_debounce_secs',
|
||||
'selfavatar',
|
||||
'sync_msgs',
|
||||
'send_sync_msgs',
|
||||
'sentbox_watch',
|
||||
'show_emails',
|
||||
'socks5_enabled',
|
||||
|
||||
@@ -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.127.0"
|
||||
"version": "1.122.0"
|
||||
}
|
||||
|
||||
@@ -24,5 +24,3 @@ ignore_missing_imports = True
|
||||
[mypy-imap_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-distutils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -195,7 +195,7 @@ class Account:
|
||||
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -203,7 +203,7 @@ class Account:
|
||||
res = lib.dc_preconfigure_keypair(
|
||||
self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
ffi.NULL,
|
||||
as_dc_charpointer(public),
|
||||
as_dc_charpointer(secret),
|
||||
)
|
||||
if res == 0:
|
||||
@@ -617,18 +617,18 @@ class Account:
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
|
||||
from .events import FFIEventLogger
|
||||
|
||||
"""get the account running, configure it if necessary. add plugins if provided.
|
||||
|
||||
:param addr: the email address of the account
|
||||
:param password: the password of the account
|
||||
:param account_plugins: a list of plugins to add
|
||||
:param show_ffi: show low level ffi events
|
||||
:param displayname: the display name of the account
|
||||
"""
|
||||
from .events import FFIEventLogger
|
||||
|
||||
if show_ffi:
|
||||
self.set_config("displayname", "bot")
|
||||
log = FFIEventLogger(self)
|
||||
self.add_account_plugin(log)
|
||||
|
||||
@@ -644,8 +644,6 @@ class Account:
|
||||
configtracker = self.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
if displayname:
|
||||
self.set_config("displayname", displayname)
|
||||
# start IO threads and configure if necessary
|
||||
self.start_io()
|
||||
|
||||
|
||||
@@ -75,12 +75,9 @@ class Contact:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||
def get_verifier(self, contact):
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||
if verifier_id == 0:
|
||||
return None
|
||||
return Contact(self.account, verifier_id)
|
||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
@@ -478,9 +478,10 @@ class ACFactory:
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
|
||||
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
|
||||
if fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_sec)
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
print(f"WARN: could not use preconfigured keys for {addr!r}")
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
@@ -137,7 +136,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
lp.sec("ac2: read member added message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.is_system_message()
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
lp.sec("ac1: send message")
|
||||
@@ -151,8 +149,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
# If we verified the contact ourselves then verifier addr == contact addr
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
@@ -177,15 +176,14 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# System message about the added member.
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_system_message()
|
||||
# Skip system message about added member
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.is_encrypted()
|
||||
@@ -526,8 +524,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
lp.sec("ac2: sending message")
|
||||
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
|
||||
# "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
lp.sec("ac1: receiving message")
|
||||
@@ -542,6 +539,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.text == msg_out.text
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
@@ -590,67 +589,4 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert msg_in.text == msg_out.text
|
||||
|
||||
|
||||
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
"""Test for the issue #4346:
|
||||
- User is added to a verified group.
|
||||
- First device of the user downloads "member added" from the group.
|
||||
- First device removes "member added" from the server.
|
||||
- Some new messages are sent to the group.
|
||||
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
|
||||
- First device re-gossips Autocrypt keys to the group.
|
||||
- Now the seconds device has all members verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
ac2.set_config("gossip_period", "0") # Re-gossip in every message
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac2.export_self_keys(str(dir))
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
# Wait for "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
|
||||
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
|
||||
lp.sec("ac1: sending 'hi' to the group")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
chat1.send_text("hi")
|
||||
|
||||
lp.sec("ac2_offl: going online, checking the 'hi' message")
|
||||
ac2_offl.start_io()
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
|
||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
assert chat2_offl.is_protected()
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
ac2.set_config("bcc_self", "0")
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message, Chat
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
|
||||
|
||||
@@ -1467,18 +1467,13 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
ac1._evtracker.wait_msg_delivered(msgs[-1])
|
||||
|
||||
for m in msgs:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
@@ -1510,7 +1505,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
time.sleep(2)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
@@ -1669,12 +1664,8 @@ def test_qr_setup_contact(acfactory, lp):
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
|
||||
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
|
||||
def test_qr_join_chat(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
|
||||
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
|
||||
|
||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
@@ -1687,17 +1678,6 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
|
||||
|
||||
# ac1 reloads the chat.
|
||||
chat = Chat(chat.account, chat.id)
|
||||
assert not chat.is_protected()
|
||||
|
||||
# ac2 reloads the chat.
|
||||
ch = Chat(ch.account, ch.id)
|
||||
assert not ch.is_protected()
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory, lp):
|
||||
"""Regression test for a bug intoduced in core v1.113.0.
|
||||
@@ -1718,10 +1698,12 @@ def test_qr_new_group_unblocked(acfactory, lp):
|
||||
|
||||
ac1_new_chat = ac1.create_group_chat("Another group")
|
||||
ac1_new_chat.add_contact(ac2)
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
|
||||
# Receive "Member added" message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
# Receive "Hello!" message.
|
||||
ac2_msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg.text == "Hello!"
|
||||
assert ac2_msg.chat.is_contact_request()
|
||||
@@ -1944,15 +1926,13 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
lp.sec("ac1: add ac2 to promoted group chat")
|
||||
chat.add_contact(ac2) # sends one message
|
||||
|
||||
lp.sec("ac2: wait for receiving member added message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
|
||||
lp.sec("ac1: send a first message to ac2")
|
||||
chat.send_text("hi") # sends another message
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("ac2: wait for receiving message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "hi"
|
||||
assert msg1.chat.id == msg2.chat.id
|
||||
|
||||
@@ -67,9 +67,10 @@ class TestOfflineAccountBasic:
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, data):
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_secret)
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from queue import Queue
|
||||
|
||||
import deltachat as dc
|
||||
@@ -222,32 +221,15 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
|
||||
def test_jsonrpc_blocking_call(tmp_path):
|
||||
accounts_fname = tmp_path / "accounts"
|
||||
writable = True
|
||||
accounts = ffi.gc(
|
||||
lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable),
|
||||
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
res = from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
|
||||
assert res == "true"
|
||||
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
|
||||
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
|
||||
assert res == "false"
|
||||
|
||||
@@ -25,9 +25,6 @@ deps =
|
||||
pytest-xdist
|
||||
pdbpp
|
||||
requests
|
||||
# urllib3 2.0 does not work in manylinux2014 containers.
|
||||
# https://github.com/deltachat/deltachat-core-rust/issues/4788
|
||||
urllib3<2
|
||||
|
||||
[testenv:.pkg]
|
||||
passenv =
|
||||
@@ -62,8 +59,7 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
|
||||
sphinx<=7.1.2
|
||||
sphinx
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-10-26
|
||||
2023-09-12
|
||||
@@ -10,8 +10,6 @@ and an own build machine.
|
||||
|
||||
- `deny.sh` runs `cargo deny` for all Rust code in the project.
|
||||
|
||||
- `codespell.sh` spellchecks the source code using `codespell` tool.
|
||||
|
||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
@@ -20,7 +18,7 @@ and an own build machine.
|
||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
||||
`run-rust-test.sh` remotely on the build machine.
|
||||
|
||||
- `make-python-testenv.sh` creates local python test development environment.
|
||||
- `make-python-testenv.sh` creates or updates local python test development environment.
|
||||
Reusing the same environment is faster than running `run-python-test.sh` which always
|
||||
recreates environment from scratch and runs additional lints.
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ resources:
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
branch: master
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
branch: master
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
tag_filter: "v*"
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ FROM $BASEIMAGE
|
||||
RUN pipx install tox
|
||||
COPY install-rust.sh /scripts/
|
||||
RUN /scripts/install-rust.sh
|
||||
RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# It rebuilds the core and bindings as needed.
|
||||
#
|
||||
# After running the script, you can either
|
||||
# run `pytest` directly with `venv/bin/pytest python/`
|
||||
# or activate the environment with `. venv/bin/activate`
|
||||
# run `pytest` directly with `env/bin/pytest python/`
|
||||
# or activate the environment with `. env/bin/activacte`
|
||||
# and run `pytest` from there.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -13,5 +13,9 @@ export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
tox -c python -e py --devenv venv
|
||||
venv/bin/pip install --upgrade pip
|
||||
if test -d env; then
|
||||
env/bin/pip install -e python --force-reinstall
|
||||
else
|
||||
tox -e py --devenv env
|
||||
env/bin/pip install --upgrade pip
|
||||
fi
|
||||
|
||||
@@ -31,7 +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,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Python wheels for deltachat-rpc-server.
|
||||
Run scripts/zig-rpc-server.sh first."""
|
||||
from pathlib import Path
|
||||
from wheel.wheelfile import WheelFile
|
||||
import tomllib
|
||||
import tarfile
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def metadata_contents(version):
|
||||
return f"""Metadata-Version: 2.1
|
||||
Name: deltachat-rpc-server
|
||||
Version: {version}
|
||||
Summary: Delta Chat JSON-RPC server
|
||||
"""
|
||||
|
||||
|
||||
def build_source_package(version):
|
||||
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
|
||||
|
||||
with tarfile.open(filename, "w:gz") as pkg:
|
||||
|
||||
def pack(name, contents):
|
||||
contents = contents.encode()
|
||||
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
|
||||
tar_info.mode = 0o644
|
||||
tar_info.size = len(contents)
|
||||
pkg.addfile(tar_info, BytesIO(contents))
|
||||
|
||||
pack("PKG-INFO", metadata_contents(version))
|
||||
pack(
|
||||
"pyproject.toml",
|
||||
f"""[build-system]
|
||||
requires = ["setuptools==68.2.2", "pip"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "{version}"
|
||||
|
||||
[project.scripts]
|
||||
deltachat-rpc-server = "deltachat_rpc_server:main"
|
||||
""",
|
||||
)
|
||||
pack(
|
||||
"setup.py",
|
||||
f"""
|
||||
import sys
|
||||
from setuptools import setup, find_packages
|
||||
from distutils.cmd import Command
|
||||
from setuptools.command.install import install
|
||||
from setuptools.command.build import build
|
||||
import subprocess
|
||||
import platform
|
||||
import tempfile
|
||||
from zipfile import ZipFile
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
|
||||
class BuildCommand(build):
|
||||
def run(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"download",
|
||||
"--no-input",
|
||||
"--timeout",
|
||||
"1000",
|
||||
"--platform",
|
||||
"musllinux_1_1_" + platform.machine(),
|
||||
"--only-binary=:all:",
|
||||
"deltachat-rpc-server=={version}",
|
||||
],
|
||||
cwd=tmpdir,
|
||||
)
|
||||
|
||||
wheel_path = next(Path(tmpdir).glob("*.whl"))
|
||||
with ZipFile(wheel_path, "r") as wheel:
|
||||
exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
|
||||
Path(exe_path).chmod(0o700)
|
||||
wheel.extract("deltachat_rpc_server/__init__.py", "src")
|
||||
|
||||
shutil.rmtree(tmpdir)
|
||||
return super().run()
|
||||
|
||||
|
||||
setup(
|
||||
cmdclass={{"build": BuildCommand}},
|
||||
package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}},
|
||||
)
|
||||
""",
|
||||
)
|
||||
pack("src/deltachat_rpc_server/__init__.py", "")
|
||||
|
||||
|
||||
def build_wheel(version, binary, tag, windows=False):
|
||||
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
|
||||
|
||||
with WheelFile(filename, "w") as wheel:
|
||||
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
|
||||
wheel.write("deltachat-rpc-server/README.md", "deltachat_rpc_server/README.md")
|
||||
if windows:
|
||||
wheel.writestr(
|
||||
"deltachat_rpc_server/__init__.py",
|
||||
"""import os, sys, subprocess
|
||||
def main():
|
||||
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server.exe"), *sys.argv[1:]]
|
||||
sys.exit(subprocess.call(argv))
|
||||
""",
|
||||
)
|
||||
else:
|
||||
wheel.writestr(
|
||||
"deltachat_rpc_server/__init__.py",
|
||||
"""import os, sys
|
||||
def main():
|
||||
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server"), *sys.argv[1:]]
|
||||
os.execv(argv[0], argv)
|
||||
""",
|
||||
)
|
||||
|
||||
Path(binary).chmod(0o755)
|
||||
wheel.write(
|
||||
binary,
|
||||
"deltachat_rpc_server/deltachat-rpc-server.exe"
|
||||
if windows
|
||||
else "deltachat_rpc_server/deltachat-rpc-server",
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/METADATA",
|
||||
metadata_contents(version),
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/WHEEL",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}",
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/entry_points.txt",
|
||||
"[console_scripts]\ndeltachat-rpc-server = deltachat_rpc_server:main",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
|
||||
cargo_toml = tomllib.load(f)
|
||||
version = cargo_toml["package"]["version"]
|
||||
Path("dist").mkdir(exist_ok=True)
|
||||
build_source_package(version)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-x86_64-linux",
|
||||
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-armv7-linux",
|
||||
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-aarch64-linux",
|
||||
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-i686-linux",
|
||||
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
)
|
||||
|
||||
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-x86_64-macos",
|
||||
"py3-none-macosx_10_7_x86_64",
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-aarch64-macos",
|
||||
"py3-none-macosx_11_0_arm64",
|
||||
)
|
||||
|
||||
build_wheel(
|
||||
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
|
||||
)
|
||||
build_wheel(
|
||||
version,
|
||||
"dist/deltachat-rpc-server-win64.exe",
|
||||
"py3-none-win_amd64",
|
||||
windows=True,
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,20 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# /// pyproject
|
||||
# [run]
|
||||
# dependencies = [
|
||||
# "ziglang==0.11.0"
|
||||
# ]
|
||||
# ///
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def flag_filter(flag: str) -> bool:
|
||||
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
|
||||
if flag == "-latomic":
|
||||
return False
|
||||
|
||||
if flag == "-lc":
|
||||
return False
|
||||
if flag == "-Wl,-melf_i386":
|
||||
@@ -34,23 +24,8 @@ def main():
|
||||
else:
|
||||
zig_cpu_args = []
|
||||
|
||||
# Disable atomics and use locks instead in OpenSSL.
|
||||
# Zig toolchains do not provide atomics.
|
||||
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
|
||||
args += ["-DBROKEN_CLANG_ATOMICS"]
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"ziglang",
|
||||
"cc",
|
||||
"-target",
|
||||
zig_target,
|
||||
*zig_cpu_args,
|
||||
*args,
|
||||
],
|
||||
check=True,
|
||||
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
28
scripts/zig-musl-check.sh
Executable file
28
scripts/zig-musl-check.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Run `cargo check` with musl libc.
|
||||
# This requires `zig` to compile vendored openssl.
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
ZIG_VERSION=0.11.0
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
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 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 check --release --target x86_64-unknown-linux-musl -p deltachat_ffi --features jsonrpc
|
||||
@@ -10,6 +10,14 @@ unset RUSTFLAGS
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
ZIG_VERSION=0.11.0
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
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" \
|
||||
@@ -42,9 +50,3 @@ 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
|
||||
|
||||
mkdir -p dist
|
||||
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
|
||||
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
|
||||
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
|
||||
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux
|
||||
|
||||
2
spec.md
2
spec.md
@@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from <sender name>`.
|
||||
Replies to messages MAY follow the typical `Re:`-format.
|
||||
|
||||
The body MAY contain text which MUST have the content type `text/plain`
|
||||
or `multipart/alternative` containing `text/plain`.
|
||||
or `mulipart/alternative` containing `text/plain`.
|
||||
|
||||
The text MAY be divided into a user-text-part and a footer-part using the
|
||||
line `-- ` (minus, minus, space, lineend).
|
||||
|
||||
208
src/accounts.rs
208
src/accounts.rs
@@ -1,16 +1,12 @@
|
||||
//! # Account manager module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::Context;
|
||||
@@ -37,16 +33,16 @@ pub struct Accounts {
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
if writable && !dir.exists() {
|
||||
pub async fn new(dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir, writable).await
|
||||
Accounts::open(dir).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
async fn create(dir: &Path) -> Result<()> {
|
||||
pub async fn create(dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
@@ -58,13 +54,13 @@ impl Accounts {
|
||||
|
||||
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
|
||||
/// no account exists and no config exists.
|
||||
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
pub async fn open(dir: PathBuf) -> Result<Self> {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
|
||||
let config = Config::from_file(config_file, writable)
|
||||
let config = Config::from_file(config_file)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
@@ -156,7 +152,7 @@ impl Accounts {
|
||||
if let Some(cfg) = self.config.get_account(id) {
|
||||
let account_path = self.dir.join(cfg.dir);
|
||||
|
||||
try_many_times(|| fs::remove_dir_all(&account_path))
|
||||
fs::remove_dir_all(&account_path)
|
||||
.await
|
||||
.context("failed to remove account data")?;
|
||||
}
|
||||
@@ -192,10 +188,10 @@ impl Accounts {
|
||||
fs::create_dir_all(self.dir.join(&account_config.dir))
|
||||
.await
|
||||
.context("failed to create dir")?;
|
||||
try_many_times(|| fs::rename(&dbfile, &new_dbfile))
|
||||
fs::rename(&dbfile, &new_dbfile)
|
||||
.await
|
||||
.context("failed to rename dbfile")?;
|
||||
try_many_times(|| fs::rename(&blobdir, &new_blobdir))
|
||||
fs::rename(&blobdir, &new_blobdir)
|
||||
.await
|
||||
.context("failed to rename blobdir")?;
|
||||
if walfile.exists() {
|
||||
@@ -220,7 +216,7 @@ impl Accounts {
|
||||
}
|
||||
Err(err) => {
|
||||
let account_path = std::path::PathBuf::from(&account_config.dir);
|
||||
try_many_times(|| fs::remove_dir_all(&account_path))
|
||||
fs::remove_dir_all(&account_path)
|
||||
.await
|
||||
.context("failed to remove account data")?;
|
||||
self.config.remove_account(account_config.id).await?;
|
||||
@@ -302,20 +298,14 @@ impl Accounts {
|
||||
/// Configuration file name.
|
||||
const CONFIG_NAME: &str = "accounts.toml";
|
||||
|
||||
/// Lockfile name.
|
||||
const LOCKFILE_NAME: &str = "accounts.lock";
|
||||
|
||||
/// Database file name.
|
||||
const DB_NAME: &str = "dc.db";
|
||||
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Config {
|
||||
file: PathBuf,
|
||||
inner: InnerConfig,
|
||||
// We lock the lockfile in the Config constructors to protect also from having multiple Config
|
||||
// objects for the same config file.
|
||||
lock_task: Option<JoinHandle<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
/// Account manager configuration file contents.
|
||||
@@ -329,74 +319,17 @@ struct InnerConfig {
|
||||
pub accounts: Vec<AccountConfig>,
|
||||
}
|
||||
|
||||
impl Drop for Config {
|
||||
fn drop(&mut self) {
|
||||
if let Some(lock_task) = self.lock_task.take() {
|
||||
lock_task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Creates a new Config for `file`, but doesn't open/sync it.
|
||||
async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
|
||||
let dir = file.parent().context("Cannot get config file directory")?;
|
||||
/// Creates a new configuration file in the given account manager directory.
|
||||
pub async fn new(dir: &Path) -> Result<Self> {
|
||||
let inner = InnerConfig {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
if !lock {
|
||||
let cfg = Self {
|
||||
file,
|
||||
inner,
|
||||
lock_task: None,
|
||||
};
|
||||
return Ok(cfg);
|
||||
}
|
||||
let lockfile = dir.join(LOCKFILE_NAME);
|
||||
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
|
||||
let (locked_tx, locked_rx) = oneshot::channel();
|
||||
let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
let mut timeout = Duration::from_millis(100);
|
||||
let _guard = loop {
|
||||
match lock.try_write() {
|
||||
Ok(guard) => break Ok(guard),
|
||||
Err(err) => {
|
||||
if timeout.as_millis() > 1600 {
|
||||
break Err(err);
|
||||
}
|
||||
// We need to wait for the previous lock_task to be aborted thus unlocking
|
||||
// the lockfile. We don't open configs for writing often outside of the
|
||||
// tests, so this adds delays to the tests, but otherwise ok.
|
||||
sleep(timeout).await;
|
||||
if err.kind() == std::io::ErrorKind::WouldBlock {
|
||||
timeout *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}?;
|
||||
locked_tx
|
||||
.send(())
|
||||
.ok()
|
||||
.context("Cannot notify about lockfile locking")?;
|
||||
let (_tx, rx) = oneshot::channel();
|
||||
rx.await?;
|
||||
Ok(())
|
||||
});
|
||||
let cfg = Self {
|
||||
file,
|
||||
inner,
|
||||
lock_task: Some(lock_task),
|
||||
};
|
||||
locked_rx.await?;
|
||||
Ok(cfg)
|
||||
}
|
||||
let file = dir.join(CONFIG_NAME);
|
||||
let mut cfg = Self { file, inner };
|
||||
|
||||
/// Creates a new configuration file in the given account manager directory.
|
||||
pub async fn new(dir: &Path) -> Result<Self> {
|
||||
let lock = true;
|
||||
let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
|
||||
cfg.sync().await?;
|
||||
|
||||
Ok(cfg)
|
||||
@@ -406,11 +339,6 @@ impl Config {
|
||||
/// 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<()> {
|
||||
ensure!(!self
|
||||
.lock_task
|
||||
.as_ref()
|
||||
.context("Config is read-only")?
|
||||
.is_finished());
|
||||
let tmp_path = self.file.with_extension("toml.tmp");
|
||||
let mut file = fs::File::create(&tmp_path)
|
||||
.await
|
||||
@@ -429,28 +357,24 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
|
||||
let dir = file
|
||||
.parent()
|
||||
.context("Cannot get config file directory")?
|
||||
.to_path_buf();
|
||||
let mut config = Self::new_nosync(file, writable).await?;
|
||||
let bytes = fs::read(&config.file)
|
||||
.await
|
||||
.context("Failed to read file")?;
|
||||
pub async fn from_file(file: PathBuf) -> Result<Self> {
|
||||
let dir = file.parent().context("can't get config file directory")?;
|
||||
let bytes = fs::read(&file).await.context("failed to read file")?;
|
||||
let s = std::str::from_utf8(&bytes)?;
|
||||
config.inner = toml::from_str(s).context("Failed to parse config")?;
|
||||
let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?;
|
||||
|
||||
// Previous versions of the core stored absolute paths in account config.
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut config.inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
|
||||
for account in &mut inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
|
||||
account.dir = new_dir.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if modified && writable {
|
||||
|
||||
let mut config = Self { file, inner };
|
||||
if modified {
|
||||
config.sync().await?;
|
||||
}
|
||||
|
||||
@@ -561,37 +485,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Spend up to 1 minute trying to do the operation.
|
||||
///
|
||||
/// Even if Delta Chat itself does not hold the file lock,
|
||||
/// there may be other processes such as antivirus,
|
||||
/// or the filesystem may be network-mounted.
|
||||
///
|
||||
/// Without this workaround removing account may fail on Windows with an error
|
||||
/// "The process cannot access the file because it is being used by another process. (os error 32)".
|
||||
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = std::result::Result<(), T>>,
|
||||
{
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
counter += 1;
|
||||
|
||||
if let Err(err) = f().await {
|
||||
if counter > 60 {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Wait 1 second and try again.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configuration of a single account.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct AccountConfig {
|
||||
@@ -625,44 +518,26 @@ mod tests {
|
||||
let p: PathBuf = dir.path().join("accounts1");
|
||||
|
||||
{
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts.add_account().await.unwrap();
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
for writable in [true, false] {
|
||||
let accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
{
|
||||
let accounts = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_open_conflict() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
let writable = true;
|
||||
let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
let writable = true;
|
||||
assert!(Accounts::new(p.clone(), writable).await.is_err());
|
||||
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p, writable).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_add_remove() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
@@ -689,8 +564,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await?;
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
@@ -711,8 +585,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
@@ -749,8 +622,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
@@ -770,8 +642,7 @@ mod tests {
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await?;
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all();
|
||||
assert_eq!(ids.len(), 1);
|
||||
@@ -806,8 +677,7 @@ mod tests {
|
||||
assert!(id2 > id1 + dummy_accounts);
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p.clone(), writable).await?;
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
@@ -852,8 +722,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(p.clone(), writable).await?;
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
@@ -879,8 +748,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable)
|
||||
let mut accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
@@ -900,8 +768,7 @@ mod tests {
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p.clone(), writable)
|
||||
let accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
@@ -925,8 +792,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await?;
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
accounts.add_account().await?;
|
||||
|
||||
|
||||
404
src/chat.rs
404
src/chat.rs
@@ -27,7 +27,6 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::events::EventType;
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -36,7 +35,6 @@ use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
@@ -44,6 +42,7 @@ use crate::tools::{
|
||||
strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -89,14 +88,6 @@ pub enum ProtectionStatus {
|
||||
///
|
||||
/// All members of the chat must be verified.
|
||||
Protected = 1,
|
||||
|
||||
/// The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
/// The user has to confirm that this is OK.
|
||||
///
|
||||
/// We only do this in 1:1 chats; in group chats, the chat just
|
||||
/// stays protected.
|
||||
ProtectionBroken = 3, // `2` was never used as a value.
|
||||
}
|
||||
|
||||
/// The reason why messages cannot be sent to the chat.
|
||||
@@ -113,10 +104,6 @@ pub(crate) enum CantSendReason {
|
||||
/// The chat is a contact request, it needs to be accepted before sending a message.
|
||||
ContactRequest,
|
||||
|
||||
/// The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
ProtectionBroken,
|
||||
|
||||
/// Mailing list without known List-Post header.
|
||||
ReadOnlyMailingList,
|
||||
|
||||
@@ -133,10 +120,6 @@ impl fmt::Display for CantSendReason {
|
||||
f,
|
||||
"contact request chat should be accepted before sending messages"
|
||||
),
|
||||
Self::ProtectionBroken => write!(
|
||||
f,
|
||||
"accept that the encryption isn't verified anymore before sending messages"
|
||||
),
|
||||
Self::ReadOnlyMailingList => {
|
||||
write!(f, "mailing list does not have a know post address")
|
||||
}
|
||||
@@ -289,7 +272,6 @@ impl ChatId {
|
||||
param: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let grpname = strip_rtlo_characters(grpname);
|
||||
let smeared_time = create_smeared_timestamp(context);
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
|
||||
@@ -298,20 +280,13 @@ impl ChatId {
|
||||
&grpname,
|
||||
grpid,
|
||||
create_blocked,
|
||||
smeared_time,
|
||||
create_smeared_timestamp(context),
|
||||
create_protected,
|
||||
param.unwrap_or_default(),
|
||||
),
|
||||
).await?;
|
||||
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
|
||||
if create_protected == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
.add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
|
||||
@@ -359,7 +334,7 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Broadcast => {
|
||||
Chattype::Undefined | Chattype::Broadcast => {
|
||||
bail!("Can't block chat of type {:?}", chat.typ)
|
||||
}
|
||||
Chattype::Single => {
|
||||
@@ -400,16 +375,7 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Single
|
||||
if chat.blocked == Blocked::Not
|
||||
&& chat.protected == ProtectionStatus::ProtectionBroken =>
|
||||
{
|
||||
// The protection was broken, then the user clicked 'Accept'/'OK',
|
||||
// so, now we want to set the status to Unprotected again:
|
||||
chat.id
|
||||
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
}
|
||||
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
|
||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
@@ -436,19 +402,20 @@ impl ChatId {
|
||||
|
||||
/// Sets protection without sending a message.
|
||||
///
|
||||
/// Returns whether the protection status was actually modified.
|
||||
/// Used when a message arrives indicating that someone else has
|
||||
/// changed the protection value for a chat.
|
||||
pub(crate) async fn inner_set_protection(
|
||||
self,
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
) -> Result<bool> {
|
||||
ensure!(!self.is_special(), "Invalid chat-id {self}.");
|
||||
) -> Result<()> {
|
||||
ensure!(!self.is_special(), "Invalid chat-id.");
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
if protect == chat.protected {
|
||||
info!(context, "Protection status unchanged for {}.", self);
|
||||
return Ok(false);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match protect {
|
||||
@@ -463,8 +430,9 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||
Chattype::Undefined => bail!("Undefined group type"),
|
||||
},
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
ProtectionStatus::Unprotected => {}
|
||||
};
|
||||
|
||||
context
|
||||
@@ -477,58 +445,68 @@ impl ChatId {
|
||||
// make sure, the receivers will get all keys
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds an info message to the chat, telling the user that the protection status changed.
|
||||
/// Send protected status message to the chat.
|
||||
///
|
||||
/// Params:
|
||||
/// This sends the message with the protected status change to the chat,
|
||||
/// notifying the user on this device as well as the other users in the chat.
|
||||
///
|
||||
/// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
|
||||
/// * `timestamp_sort` is used as the timestamp of the added message
|
||||
/// and should be the timestamp of the change happening.
|
||||
/// If `promote` is false this means, the message must not be sent out
|
||||
/// and only a local info message should be added to the chat.
|
||||
/// This is used when protection is enabled implicitly or when a chat is not yet promoted.
|
||||
pub(crate) async fn add_protection_msg(
|
||||
self,
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
contact_id: Option<ContactId>,
|
||||
timestamp_sort: i64,
|
||||
promote: bool,
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let text = context.stock_protection_msg(protect, contact_id).await;
|
||||
let text = context.stock_protection_msg(protect, from_id).await;
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
|
||||
};
|
||||
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
|
||||
|
||||
if promote {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(cmd);
|
||||
send_msg(context, self, &mut msg).await?;
|
||||
} else {
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
cmd,
|
||||
create_smeared_timestamp(context),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and sends or adds a message.
|
||||
///
|
||||
/// `timestamp_sort` is used as the timestamp of the added message
|
||||
/// and should be the timestamp of the change happening.
|
||||
pub(crate) async fn set_protection(
|
||||
self,
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
timestamp_sort: i64,
|
||||
contact_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
match self.inner_set_protection(context, protect).await {
|
||||
Ok(protection_status_modified) => {
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
|
||||
Err(e)
|
||||
}
|
||||
pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> {
|
||||
ensure!(!self.is_special(), "set protection: invalid chat-id.");
|
||||
|
||||
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
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Archives or unarchives a chat.
|
||||
@@ -765,6 +743,14 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
|
||||
bail!(
|
||||
"Can't set a draft because chat is not writeable: {}",
|
||||
cant_send_reason
|
||||
);
|
||||
}
|
||||
|
||||
// set back draft information to allow identifying the draft later on -
|
||||
// no matter if message object is reused or reloaded from db
|
||||
msg.state = MessageState::OutDraft;
|
||||
@@ -932,10 +918,9 @@ impl ChatId {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT chat_id, count(*) AS n
|
||||
FROM chats_contacts
|
||||
WHERE contact_id > ? AND chat_id > ?
|
||||
FROM chats_contacts where contact_id > 9
|
||||
GROUP BY chat_id",
|
||||
(ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
|
||||
(),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let size: f64 = row.get(1)?;
|
||||
@@ -1274,7 +1259,7 @@ pub struct Chat {
|
||||
pub grpid: String,
|
||||
|
||||
/// Whether the chat is blocked, unblocked or a contact request.
|
||||
pub blocked: Blocked,
|
||||
pub(crate) blocked: Blocked,
|
||||
|
||||
/// Additional chat parameters stored in the database.
|
||||
pub param: Params,
|
||||
@@ -1286,7 +1271,7 @@ pub struct Chat {
|
||||
pub mute_duration: MuteDuration,
|
||||
|
||||
/// If the chat is protected (verified).
|
||||
pub(crate) protected: ProtectionStatus,
|
||||
protected: ProtectionStatus,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
@@ -1381,8 +1366,6 @@ impl Chat {
|
||||
Some(DeviceChat)
|
||||
} else if self.is_contact_request() {
|
||||
Some(ContactRequest)
|
||||
} else if self.is_protection_broken() {
|
||||
Some(ProtectionBroken)
|
||||
} else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
|
||||
Some(ReadOnlyMailingList)
|
||||
} else if !self.is_self_in_chat(context).await? {
|
||||
@@ -1407,6 +1390,7 @@ impl Chat {
|
||||
match self.typ {
|
||||
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
|
||||
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
||||
Chattype::Undefined => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1545,27 +1529,6 @@ impl Chat {
|
||||
self.protected == ProtectionStatus::Protected
|
||||
}
|
||||
|
||||
/// Returns true if the chat was protected, and then an incoming message broke this protection.
|
||||
///
|
||||
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
/// otherwise it will return false for all chats.
|
||||
///
|
||||
/// 1:1 chats are automatically set as protected when a contact is verified.
|
||||
/// When a message comes in that is not encrypted / signed correctly,
|
||||
/// the chat is automatically set as unprotected again.
|
||||
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
|
||||
///
|
||||
/// The UI should let the user confirm that this is OK with a message like
|
||||
/// `Bob sent a message from another device. Tap to learn more`
|
||||
/// and then call `chat_id.accept()`.
|
||||
pub fn is_protection_broken(&self) -> bool {
|
||||
match self.protected {
|
||||
ProtectionStatus::Protected => false,
|
||||
ProtectionStatus::Unprotected => false,
|
||||
ProtectionStatus::ProtectionBroken => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if location streaming is enabled in the chat.
|
||||
pub fn is_sending_locations(&self) -> bool {
|
||||
self.is_sending_locations
|
||||
@@ -1596,6 +1559,15 @@ impl Chat {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
if let Some(reason) = self.why_cant_send(context).await? {
|
||||
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot send message; self not in group.".into(),
|
||||
));
|
||||
}
|
||||
bail!("Cannot send message to {}: {}", self.id, reason);
|
||||
}
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let new_rfc724_mid = {
|
||||
let grpid = match self.typ {
|
||||
@@ -2116,30 +2088,19 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
let protected = peerstate.map_or(false, |p| {
|
||||
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
|
||||
});
|
||||
let smeared_time = create_smeared_timestamp(context);
|
||||
|
||||
let chat_id = context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute(
|
||||
"INSERT INTO chats
|
||||
(type, name, param, blocked, created_timestamp, protected)
|
||||
VALUES(?, ?, ?, ?, ?, ?)",
|
||||
(type, name, param, blocked, created_timestamp)
|
||||
VALUES(?, ?, ?, ?, ?)",
|
||||
(
|
||||
Chattype::Single,
|
||||
chat_name,
|
||||
params.to_string(),
|
||||
create_blocked as u8,
|
||||
smeared_time,
|
||||
if protected {
|
||||
ProtectionStatus::Protected
|
||||
} else {
|
||||
ProtectionStatus::Unprotected
|
||||
},
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(
|
||||
@@ -2160,17 +2121,6 @@ impl ChatIdBlocked {
|
||||
})
|
||||
.await?;
|
||||
|
||||
if protected {
|
||||
chat_id
|
||||
.add_protection_msg(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
Some(contact_id),
|
||||
smeared_time,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
match contact_id {
|
||||
ContactId::SELF => update_saved_messages_icon(context).await?,
|
||||
ContactId::DEVICE => update_device_icon(context).await?,
|
||||
@@ -2208,12 +2158,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
|
||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||
if msg.viewtype == Viewtype::Image
|
||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
|
||||
{
|
||||
if msg.viewtype == Viewtype::Image || maybe_sticker {
|
||||
blob.recode_to_image_size(context, &mut maybe_sticker)
|
||||
.await?;
|
||||
|
||||
if !maybe_sticker {
|
||||
msg.viewtype = Viewtype::Image;
|
||||
}
|
||||
@@ -2287,13 +2234,7 @@ async fn prepare_msg_common(
|
||||
|
||||
// Check if the chat can be sent to.
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
if reason == CantSendReason::ProtectionBroken
|
||||
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
{
|
||||
// Send out the message, the securejoin message is supposed to repair the verification
|
||||
} else {
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
}
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
@@ -2395,7 +2336,7 @@ 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 against RTLO attacks
|
||||
// protect all system messages againts RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
msg.text = strip_rtlo_characters(&msg.text);
|
||||
}
|
||||
@@ -2517,13 +2458,14 @@ pub(crate) async fn create_send_msg_job(
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
|
||||
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:#}.");
|
||||
}
|
||||
if !msg.hidden {
|
||||
if let Err(err) =
|
||||
location::set_msg_location_id(context, msg.id, last_added_location_id).await
|
||||
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
|
||||
.await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {err:#}.");
|
||||
}
|
||||
@@ -2955,9 +2897,6 @@ pub enum Direction {
|
||||
}
|
||||
|
||||
/// Searches next/previous message based on the given message and list of types.
|
||||
///
|
||||
/// Deprecated since 2023-10-03.
|
||||
#[deprecated(note = "use `get_chat_media` instead")]
|
||||
pub async fn get_next_media(
|
||||
context: &Context,
|
||||
curr_msg_id: MsgId,
|
||||
@@ -3036,14 +2975,18 @@ pub async fn create_group_chat(
|
||||
|
||||
let grpid = create_id();
|
||||
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
(Chattype::Group, chat_name, grpid, timestamp),
|
||||
(
|
||||
Chattype::Group,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3055,9 +2998,9 @@ pub async fn create_group_chat(
|
||||
context.emit_msgs_changed_without_ids();
|
||||
|
||||
if protect == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
.set_protection(context, protect, timestamp, None)
|
||||
.await?;
|
||||
// this part is to stay compatible to verified groups,
|
||||
// in some future, we will drop the "protect"-flag from create_group_chat()
|
||||
chat_id.inner_set_protection(context, protect).await?;
|
||||
}
|
||||
|
||||
Ok(chat_id)
|
||||
@@ -3585,8 +3528,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcDocument);
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
@@ -5313,6 +5254,72 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_protection() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
assert_eq!(msg.get_state(), MessageState::InNoticed);
|
||||
|
||||
// disable protection again, still unpromoted
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
|
||||
assert_eq!(msg.get_state(), MessageState::InNoticed);
|
||||
|
||||
// send a message, this switches to promoted state
|
||||
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id).await?;
|
||||
assert_eq!(msgs.len(), 3);
|
||||
|
||||
// enable protection on promoted chat, the info-message is sent via send_msg() this time
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lookup_by_contact_id() {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
@@ -5682,51 +5689,6 @@ mod tests {
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_jpeg_force() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
let file = alice.get_blobdir().join("sticker.jpg");
|
||||
tokio::fs::write(
|
||||
&file,
|
||||
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Images without force_sticker should be turned into [Viewtype::Image]
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Image);
|
||||
|
||||
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
msg.force_sticker();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
|
||||
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
|
||||
// even on drafted messages
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
msg.force_sticker();
|
||||
alice_chat
|
||||
.id
|
||||
.set_draft(&alice, Some(&mut msg))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
@@ -6126,40 +6088,22 @@ mod tests {
|
||||
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
|
||||
{
|
||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
||||
assert_eq!(chat.name, "Broadcast list");
|
||||
assert!(!chat.is_self_talk());
|
||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
||||
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
|
||||
assert!(!chat.is_self_talk());
|
||||
|
||||
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
}
|
||||
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
|
||||
{
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), "ola!");
|
||||
assert_eq!(msg.subject, "Broadcast list");
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_ne!(chat.id, chat_bob.id);
|
||||
assert_eq!(chat.name, "Broadcast list");
|
||||
assert!(!chat.is_self_talk());
|
||||
}
|
||||
|
||||
{
|
||||
// Alice changes the name:
|
||||
set_chat_name(&alice, broadcast_id, "My great broadcast").await?;
|
||||
let sent = alice.send_text(broadcast_id, "I changed the title!").await;
|
||||
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.subject, "Re: My great broadcast");
|
||||
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(bob_chat.name, "My great broadcast");
|
||||
}
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), "ola!");
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.id, chat_bob.id);
|
||||
assert!(!chat.is_self_talk());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! # Chat list module.
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::constants::{
|
||||
@@ -16,10 +15,6 @@ use crate::stock_str;
|
||||
use crate::summary::Summary;
|
||||
use crate::tools::IsNoneOrEmpty;
|
||||
|
||||
/// Regex to find out if a query should filter by unread messages.
|
||||
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
///
|
||||
/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
|
||||
@@ -83,8 +78,7 @@ impl Chatlist {
|
||||
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||
/// is added as needed.
|
||||
/// `query`: An optional query for filtering the list. Only chats matching this query
|
||||
/// are returned. When `is:unread` is contained in the query, the chatlist is
|
||||
/// filtered such that only chats with unread messages show up.
|
||||
/// are returned.
|
||||
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
|
||||
/// are returned.
|
||||
pub async fn try_load(
|
||||
@@ -178,10 +172,8 @@ impl Chatlist {
|
||||
)
|
||||
.await?
|
||||
} else if let Some(query) = query {
|
||||
let mut query = query.trim().to_string();
|
||||
ensure!(!query.is_empty(), "query mustn't be empty");
|
||||
let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
|
||||
query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
|
||||
let query = query.trim().to_string();
|
||||
ensure!(!query.is_empty(), "missing query");
|
||||
|
||||
// allow searching over special names that may change at any time
|
||||
// when the ui calls set_stock_translation()
|
||||
@@ -206,10 +198,9 @@ impl Chatlist {
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked!=1
|
||||
AND c.name LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -406,7 +397,7 @@ impl Chatlist {
|
||||
.context("loading contact failed")?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
Chattype::Single => (Some(lastmsg), None),
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -471,8 +462,7 @@ pub async fn get_last_message_for_chat(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, ProtectionStatus,
|
||||
create_group_chat, get_chat_contacts, remove_contact_from_chat, ProtectionStatus,
|
||||
};
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -481,7 +471,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
let t = TestContext::new_bob().await;
|
||||
let t = TestContext::new().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -520,31 +510,6 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
// receive a message from alice
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
|
||||
.await
|
||||
.unwrap();
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "bob", "bob@example.net")
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_text_msg(&alice, alice_chat_id, "hi".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
|
||||
t.recv_msg(&sent_msg).await;
|
||||
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(chats.len() == 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -286,20 +286,15 @@ pub enum Config {
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
|
||||
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing "fake idle".
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
|
||||
/// Send sync messages, requires `BccSelf` to be set as well.
|
||||
/// In a future versions, this switch may be removed.
|
||||
#[strum(props(default = "0"))]
|
||||
SyncMsgs,
|
||||
SendSyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
/// may be the one of our email server.
|
||||
@@ -317,23 +312,6 @@ pub enum Config {
|
||||
|
||||
/// Last message processed by the bot.
|
||||
LastMsgId,
|
||||
|
||||
/// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by
|
||||
/// default.
|
||||
///
|
||||
/// This is not supposed to be changed by UIs and only used for testing.
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// to 1 if it supports verified 1:1 chats.
|
||||
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
|
||||
/// and when the key changes, an info message is posted into the chat.
|
||||
/// 0=Nothing else happens when the key changes.
|
||||
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
|
||||
/// until `chat_id.accept()` is called.
|
||||
#[strum(props(default = "0"))]
|
||||
VerifiedOneOnOneChats,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -487,28 +465,6 @@ impl Context {
|
||||
.set_raw_config(key.as_ref(), value.as_deref())
|
||||
.await?;
|
||||
}
|
||||
Config::Socks5Enabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::FetchExistingMsgs
|
||||
| Config::DeleteToTrash
|
||||
| Config::SaveMimeHeaders
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
| Config::SyncMsgs
|
||||
| Config::SignUnencrypted
|
||||
| Config::DisableIdle => {
|
||||
ensure!(
|
||||
matches!(value, None | Some("0") | Some("1")),
|
||||
"Boolean value must be either 0 or 1"
|
||||
);
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
@@ -658,18 +614,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that "bot" config can only be set to "0" or "1".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_bot() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert!(t.set_config(Config::Bot, None).await.is_ok());
|
||||
assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
|
||||
assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
|
||||
assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
|
||||
assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_media_quality_config_option() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::config::Config;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::job;
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::message::{Message, Viewtype};
|
||||
@@ -465,7 +466,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
if configured_addr != param.addr {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
info!(ctx, "Scheduling resync because the address has changed.");
|
||||
ctx.schedule_resync().await?;
|
||||
job::schedule_resync(ctx).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
/// Chat type.
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
@@ -140,6 +141,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Chattype {
|
||||
/// Undefined chat type.
|
||||
#[default]
|
||||
Undefined = 0,
|
||||
|
||||
/// 1:1 chat.
|
||||
Single = 100,
|
||||
|
||||
@@ -218,6 +223,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_chattype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Chattype::Undefined, Chattype::default());
|
||||
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
|
||||
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
||||
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
|
||||
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
@@ -109,7 +109,7 @@ impl ContactId {
|
||||
|
||||
/// ID of the contact for device messages.
|
||||
pub const DEVICE: ContactId = ContactId::new(5);
|
||||
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
|
||||
/// Address to go with [`ContactId::DEVICE`].
|
||||
///
|
||||
@@ -139,15 +139,6 @@ impl ContactId {
|
||||
pub const fn to_u32(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Mark contact as bot.
|
||||
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE contacts SET is_bot=? WHERE id=?;", (is_bot, self.0))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
@@ -232,9 +223,6 @@ pub struct Contact {
|
||||
|
||||
/// Last seen message signature for this contact, to be displayed in the profile.
|
||||
status: String,
|
||||
|
||||
/// If the contact is a bot.
|
||||
is_bot: bool,
|
||||
}
|
||||
|
||||
/// Possible origins of a contact.
|
||||
@@ -378,7 +366,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status, c.is_bot
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
(contact_id,),
|
||||
@@ -391,7 +379,6 @@ impl Contact {
|
||||
let authname: String = row.get(5)?;
|
||||
let param: String = row.get(6)?;
|
||||
let status: Option<String> = row.get(7)?;
|
||||
let is_bot: bool = row.get(8)?;
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name,
|
||||
@@ -402,7 +389,6 @@ impl Contact {
|
||||
origin,
|
||||
param: param.parse().unwrap_or_default(),
|
||||
status: status.unwrap_or_default(),
|
||||
is_bot,
|
||||
};
|
||||
Ok(contact)
|
||||
},
|
||||
@@ -512,11 +498,6 @@ impl Contact {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether contact is a bot.
|
||||
pub fn is_bot(&self) -> bool {
|
||||
self.is_bot
|
||||
}
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked contact.
|
||||
///
|
||||
/// Known and unblocked contacts will be returned by `get_contacts()`.
|
||||
@@ -831,11 +812,7 @@ impl Contact {
|
||||
let mut ret = Vec::new();
|
||||
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
|
||||
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
||||
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
|
||||
Origin::Unknown
|
||||
} else {
|
||||
Origin::IncomingReplyTo
|
||||
};
|
||||
|
||||
if flag_verified_only || query.is_some() {
|
||||
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
|
||||
context
|
||||
@@ -855,7 +832,7 @@ impl Contact {
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
minimal_origin,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 }
|
||||
@@ -905,10 +882,10 @@ 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![ContactId::LAST_SPECIAL, minimal_origin]),
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo
|
||||
])),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
@@ -1032,7 +1009,7 @@ impl Contact {
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
ret += &format!("{stock_message}.\n{finger_prints}:");
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
let fingerprint_self = SignedPublicKey::load_self(context)
|
||||
.await?
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
@@ -1234,15 +1211,31 @@ impl Contact {
|
||||
/// and if the key has not changed since this verification.
|
||||
///
|
||||
/// The UI may draw a checkbox or something like that beside verified contacts.
|
||||
///
|
||||
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
|
||||
self.is_verified_ex(context, None).await
|
||||
}
|
||||
|
||||
/// Same as `Contact::is_verified` but allows speeding up things
|
||||
/// by adding the peerstate belonging to the contact.
|
||||
/// If you do not have the peerstate available, it is loaded automatically.
|
||||
pub async fn is_verified_ex(
|
||||
&self,
|
||||
context: &Context,
|
||||
peerstate: Option<&Peerstate>,
|
||||
) -> Result<VerifiedStatus> {
|
||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||
// on this device
|
||||
if self.id == ContactId::SELF {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
if peerstate.is_using_verified_key() {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
}
|
||||
@@ -1259,22 +1252,11 @@ impl Contact {
|
||||
|
||||
/// Returns the ContactId that verified the contact.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if verifier_addr == self.addr {
|
||||
// Contact is directly verified via QR code.
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
|
||||
Some(contact_id) => Ok(Some(contact_id)),
|
||||
None => {
|
||||
let addr = &self.addr;
|
||||
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
|
||||
Ok(None)
|
||||
}
|
||||
let verifier_addr = self.get_verifier_addr(context).await?;
|
||||
if let Some(addr) = verifier_addr {
|
||||
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
@@ -19,11 +19,11 @@ use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::Contact;
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{load_self_public_key, DcKey as _};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::{InterruptInfo, SchedulerState};
|
||||
use crate::scheduler::SchedulerState;
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
@@ -38,7 +38,7 @@ use crate::tools::{duration_to_str, time};
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Creating a new unencrypted database:
|
||||
/// Creating a new unecrypted database:
|
||||
///
|
||||
/// ```
|
||||
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -211,6 +211,9 @@ pub struct InnerContext {
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
|
||||
/// Set to true if quota update is requested.
|
||||
pub(crate) quota_update_request: AtomicBool,
|
||||
|
||||
/// IMAP UID resync request.
|
||||
pub(crate) resync_request: AtomicBool,
|
||||
|
||||
@@ -379,8 +382,9 @@ impl Context {
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
quota: RwLock::new(None),
|
||||
quota_update_request: AtomicBool::new(false),
|
||||
resync_request: AtomicBool::new(false),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
@@ -422,16 +426,6 @@ impl Context {
|
||||
self.scheduler.maybe_network().await;
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(&self) -> Result<()> {
|
||||
self.resync_request.store(true, Ordering::Relaxed);
|
||||
self.scheduler
|
||||
.interrupt_inbox(InterruptInfo {
|
||||
probe_network: false,
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
///
|
||||
/// Warning: this is only here for testing, not part of the public API.
|
||||
@@ -584,8 +578,7 @@ impl Context {
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
|
||||
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
|
||||
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||
|
||||
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
|
||||
|
||||
@@ -593,7 +586,7 @@ impl Context {
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM acpeerstates;", ())
|
||||
.await?;
|
||||
let fingerprint_str = match load_self_public_key(self).await {
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
@@ -697,8 +690,7 @@ impl Context {
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("send_sync_msgs", send_sync_msgs.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
@@ -760,6 +752,7 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
res.insert(
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
@@ -768,16 +761,6 @@ impl Context {
|
||||
"last_msg_id",
|
||||
self.get_config_int(Config::LastMsgId).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"gossip_period",
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats",
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
@@ -831,22 +814,7 @@ impl Context {
|
||||
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 => {
|
||||
// If `last_msg_id` is not set yet,
|
||||
// subtract 1 from the last id,
|
||||
// so a single message is returned and can
|
||||
// be marked as seen.
|
||||
self.sql
|
||||
.query_row(
|
||||
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => MsgId::new_unset(),
|
||||
};
|
||||
|
||||
let list = self
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
//! Forward log messages to logging webxdc
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
use crate::webxdc::StatusUpdateItem;
|
||||
use crate::{
|
||||
chat::ChatId,
|
||||
config::Config,
|
||||
context::Context,
|
||||
message::{Message, MsgId, Viewtype},
|
||||
param::Param,
|
||||
tools::time,
|
||||
webxdc::StatusUpdateItem,
|
||||
EventType,
|
||||
};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
@@ -25,8 +26,8 @@ use crate::pgp;
|
||||
pub fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: &[SignedSecretKey],
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
private_keyring: &Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match {
|
||||
let mime = get_autocrypt_mime(mail);
|
||||
@@ -226,8 +227,8 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
|
||||
/// Returns Ok(None) if nothing encrypted was found.
|
||||
fn decrypt_part(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: &[SignedSecretKey],
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
private_keyring: &Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
@@ -262,7 +263,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
|
||||
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
|
||||
pub(crate) fn validate_detached_signature<'a, 'b>(
|
||||
mail: &'a ParsedMail<'b>,
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return None;
|
||||
@@ -282,13 +283,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
|
||||
let mut public_keyring_for_validate = Vec::new();
|
||||
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring<SignedPublicKey> {
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.push(key.clone());
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
public_keyring_for_validate.push(key.clone());
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
}
|
||||
}
|
||||
public_keyring_for_validate
|
||||
|
||||
101
src/download.rs
101
src/download.rs
@@ -10,11 +10,11 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::job::{self, Action, Job, Status};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::tools::time;
|
||||
use crate::{stock_str, EventType};
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
@@ -23,7 +23,7 @@ use crate::{stock_str, EventType};
|
||||
/// eg. to assign them to the correct chat.
|
||||
/// As these messages are typically small,
|
||||
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
@@ -90,14 +90,7 @@ impl MsgId {
|
||||
DownloadState::Available | DownloadState::Failure => {
|
||||
self.update_download_state(context, DownloadState::InProgress)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
|
||||
.await?;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
job::add(context, Job::new(Action::DownloadMsg, self.to_u32())).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -131,49 +124,59 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Actually download a message partially downloaded before.
|
||||
///
|
||||
/// Most messages are downloaded automatically on fetch instead.
|
||||
pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Imap) -> Result<()> {
|
||||
imap.prepare(context).await?;
|
||||
impl Job {
|
||||
/// Actually download a message.
|
||||
/// Called in response to `Action::DownloadMsg`.
|
||||
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "download: could not connect: {:#}", err);
|
||||
return Status::RetryNow;
|
||||
}
|
||||
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
(&msg.rfc724_mid,),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
Ok((server_uid, server_folder))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
let row = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
|
||||
(&msg.rfc724_mid,),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
Ok((server_uid, server_folder))
|
||||
}
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await?;
|
||||
Err(anyhow!("Call download_full() again to try over."))
|
||||
}
|
||||
ImapActionResult::Success => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Ok(())
|
||||
}
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
} else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await?;
|
||||
Err(anyhow!("Call download_full() again to try over."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user