Compare commits

..

1 Commits

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

8
.gitattributes vendored
View File

@@ -2,14 +2,6 @@
# ensures this even if the user has not set core.autocrlf.
* text=auto
# Checkout JavaScript files with LF line endings
# to prevent `prettier` from reporting errors on Windows.
*.js eol=lf
*.jsx eol=lf
*.ts eol=lf
*.tsx eol=lf
*.json eol=lf
# This directory contains email messages verbatim, and changing CRLF to
# LF will corrupt them.
test-data/** text=false

View File

@@ -5,5 +5,5 @@ updates:
schedule:
interval: "monthly"
commit-message:
prefix: "chore(cargo)"
prefix: "cargo"
open-pull-requests-limit: 50

26
.github/mergeable.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
version: 2
mergeable:
- when: pull_request.*
name: "Changelog check"
validate:
- do: or
validate:
- do: description
must_include:
regex: "#skip-changelog"
- do: and
validate:
- do: dependent
changed:
file: "src/**"
required: ["CHANGELOG.md"]
- do: dependent
changed:
file: "deltachat-ffi/src/**"
required: ["CHANGELOG.md"]
fail:
- do: checks
status: "action_required"
payload:
title: Changelog might need an update
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
# GitHub Actions workflow
# to automatically approve PRs made by Dependabot.
name: Dependabot auto-approve
on: pull_request

View File

@@ -38,12 +38,13 @@ jobs:
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
run: |
cd deltachat-jsonrpc/typescript
npm install --ignore-scripts
- name: Package
shell: bash
working-directory: deltachat-jsonrpc/typescript
run: |
cd deltachat-jsonrpc/typescript
npm run build
npm pack .
ls -lah

View File

@@ -22,19 +22,24 @@ jobs:
- name: Add Rust cache
uses: Swatinem/rust-cache@v2
- name: npm install
working-directory: deltachat-jsonrpc/typescript
run: npm install
run: |
cd deltachat-jsonrpc/typescript
npm install
- name: Build TypeScript, run Rust tests, generate bindings
working-directory: deltachat-jsonrpc/typescript
run: npm run build
run: |
cd deltachat-jsonrpc/typescript
npm run build
- name: Run integration tests
working-directory: deltachat-jsonrpc/typescript
run: npm run test
run: |
cd deltachat-jsonrpc/typescript
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver
run: |
cd deltachat-jsonrpc
cargo build --bin deltachat-jsonrpc-server --features webserver
- name: Run linter
working-directory: deltachat-jsonrpc/typescript
run: npm run prettier:check
run: |
cd deltachat-jsonrpc/typescript
npm run prettier:check

View File

@@ -0,0 +1,31 @@
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
name: Delete node PR previews
on:
pull_request:
types: [closed]
jobs:
delete:
runs-on: ubuntu-latest
steps:
- name: Get Pull Request ID
id: getid
run: |
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
- name: Renaming
run: |
# create empty file to copy it over the outdated deliverable on download.delta.chat
echo "This preview build is outdated and has been removed." > empty
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
- name: Replace builds with dummy files
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
host: "download.delta.chat"
port: 22
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
remote: "/var/www/html/download/node/preview/"

View File

@@ -1,8 +1,3 @@
# GitHub Actions workflow to build
# Node.js bindings documentation
# and upload it to the web server.
# Built documentation is available at <https://js.delta.chat/>
name: Generate & upload node.js documentation
on:
@@ -22,8 +17,8 @@ jobs:
node-version: 16.x
- name: npm install and generate documentation
working-directory: node
run: |
cd node
npm i --ignore-scripts
npx typedoc
mv docs js

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest]
os: [ubuntu-20.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -46,12 +46,13 @@ jobs:
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
working-directory: node
run: npm install --verbose
run: |
cd node
npm install --verbose
- name: Build Prebuild
working-directory: node
run: |
cd node
npm run prebuildify
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
@@ -61,81 +62,10 @@ jobs:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
prebuild-linux:
name: Prebuild Linux
runs-on: ubuntu-latest
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
container: ubuntu:18.04
steps:
# Working directory is owned by 1001:1001 by default.
# Change it to our user.
- name: Change working directory owner
run: chown root:root .
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
- run: apt-get update
# Python is needed for node-gyp
- name: Install curl, python and compilers
run: apt-get install -y curl build-essential python3
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: System info
run: |
rustc -vV
rustup -vV
cargo -vV
npm --version
node --version
- name: Cache node modules
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
~/.npm
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
working-directory: node
run: npm install --verbose
- name: Build Prebuild
working-directory: node
run: |
npm run prebuildify
tar -zcvf "linux.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v3
with:
name: linux
path: node/linux.tar.gz
pack-module:
needs: [prebuild, prebuild-linux]
needs: prebuild
name: Package deltachat-node and upload to download.delta.chat
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Install tree
run: sudo apt install tree
@@ -166,10 +96,10 @@ jobs:
npm --version
node --version
echo $DELTACHAT_NODE_TAR_GZ
- name: Download Linux prebuild
- name: Download Ubuntu prebuild
uses: actions/download-artifact@v1
with:
name: linux
name: ubuntu-20.04
- name: Download macOS prebuild
uses: actions/download-artifact@v1
with:
@@ -181,11 +111,11 @@ jobs:
- shell: bash
run: |
mkdir node/prebuilds
tar -xvzf linux/linux.tar.gz -C node/prebuilds
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -rf linux macos-latest windows-latest
rm -rf ubuntu-20.04 macos-latest windows-latest
- name: Install dependencies without running scripts
run: |
npm install --ignore-scripts

View File

@@ -1,6 +1,3 @@
# GitHub Actions workflow
# to test Node.js bindings.
name: "node.js tests"
# Cancel previously started workflow runs
@@ -55,13 +52,25 @@ jobs:
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
working-directory: node
run: npm install --verbose
run: |
cd node
npm install --verbose
- name: Test
timeout-minutes: 10
working-directory: node
run: npm run test
if: runner.os != 'Windows'
run: |
cd node
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
- name: Run tests on Windows, except lint
timeout-minutes: 10
if: runner.os == 'Windows'
run: |
cd node
npm run test:mocha
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,5 +1,4 @@
# Manually triggered GitHub Actions workflow
# to build a Windows repl.exe which users can
# Manually triggered action to build a Windows repl.exe which users can
# download to debug complex bugs.
name: Build Windows REPL .exe

View File

@@ -1,7 +1,3 @@
# GitHub Actions workflow
# to build `deltachat_fii` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat
on:

View File

@@ -1,494 +1,18 @@
# Changelog
## [1.122.0] - 2023-09-12
### API-Changes
- jsonrpc: Return only chat IDs for similar chats.
### Fixes
- Reopen all connections on database passpharse change.
- Do not block new group chats if 1:1 chat is blocked.
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
### Build system
- Update to OpenSSL 3.0.
- Bump webpki from 0.22.0 to 0.22.1.
- python: Add link to Mastodon into projects.urls.
### Features / Changes
- Add RSA-4096 key generation support.
### Refactor
- pgp: Add constants for encryption algorithm and hash.
## [1.121.0] - 2023-09-06
### API-Changes
- Add `dc_context_change_passphrase()`.
- Add `Message.set_file_from_bytes()` API.
- Add experimental API to get similar chats.
### Build system
- Build node packages on Ubuntu 18.04 instead of Debian 10.
This reduces the requirement for glibc version from 2.28 to 2.27.
### Fixes
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
- 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)).
- Do not allow dots at the end of email addresses.
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
## [1.120.0] - 2023-08-28
### API-Changes
- jsonrpc: Add `resend_messages`.
### Fixes
- Update async-imap to 0.9.1 to fix memory leak.
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
### Refactor
- Hide accounts.rs constants from public API.
- Hide pgp module from public API.
### Build system
- Update to Zig 0.11.0.
- Update to Rust 1.72.0.
### CI
- Run on push to stable branch.
### Miscellaneous Tasks
- python: Fix lint errors.
- python: Fix `ruff` 0.0.286 warnings.
- Fix beta clippy warnings.
## [1.119.1] - 2023-08-06
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
### Features / Changes
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
### Fixes
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
### Tests
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
## [1.119.0] - 2023-08-03
### Fixes
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
- imap: Do not resync IMAP after initial configuration.
- webxdc: Accept WebXDC updates in mailing lists.
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
- webxdc: Delete old webxdc status updates during housekeeping.
- Return valid MsgId from `receive_imf()` when the message is replaced.
- Emit MsgsChanged event with correct chat id for replaced messages.
- deltachat-rpc-server: Update tokio-tar to fix backup import.
### Features / Changes
- deltachat-rpc-client: Add `MSG_DELETED` constant.
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
### API-Changes
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
### Documentation
- Update default value for `show_emails` in `dc_set_config()` documentation.
### Refactor
- Improve IMAP logs.
### Tests
- Add basic import/export test for async python.
- Add `test_webxdc_download_on_demand`.
- Add tests for deletion of webxdc status-updates.
## [1.118.0] - 2023-07-07
### API-Changes
- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
- Add `Contact::get_by_id_optional()` API.
- [**breaking**] Make `Message.text` non-optional.
- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
Previously `handle_mdn` was erroneously exposed in the public API.
- python: flatten the API of `deltachat` module.
### Fixes
- Use different member added/removal messages locally and on the network.
- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
- Make SVG avatar image work on more platforms (use `xlink:href`).
- Preserve indentation when converting plaintext to HTML.
- Do not run simplify() on dehtml() output.
- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
### Documentation
- Document how to regenerate Node.js constants before the release.
### Build system
- git-cliff: Do not fail if commit.footers is undefined.
### Other
- Dependency updates.
- Update MPL 2.0 license text.
- Add LICENSE file to deltachat-rpc-client.
- deltachat-rpc-client: Add Trove classifiers.
- python: Change bindings status to production/stable.
### Tests
- Add `make-python-testenv.sh` script.
## [1.117.0] - 2023-06-15
### Features
- New group membership update algorithm.
New algorithm improves group consistency
in cases of missing messages,
restored old backups and replies from classic MUAs.
- Add `DC_EVENT_MSG_DELETED` event.
This event notifies the UI about the message
being deleted from the messagelist, e.g. when the message expires
or the user deletes it.
### Fixes
- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires.
Specifying msg IDs that cannot be loaded in the event payload
results in an error when the UI tries to load the message.
Instead, emit an event without IDs
to make the UI reload the whole messagelist.
- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`.
This bug resulted in failure to propagate verification
if the contact list already contained a new verified group member
with a non-lowercase address.
- dehtml: skip links with empty text.
Links like `<a href="https://delta.chat/"></a>` in HTML mails are now skipped
instead of being converted to a link without a label like `[](https://delta.chat/)`.
- dehtml: Do not insert unnecessary newlines when parsing `<p>` tags.
- Update from yanked `libc` 0.2.145 to 0.2.146.
- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency.
### API-Changes
- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted.
Previously a single event with zero chat ID was emitted.
- python: make `Contact.is_verified()` return bool.
- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)).
- rust: make `WebxdcManifest` type public.
### Build system
- Use Rust 1.70.0 to compile deltachat-rpc-server releases.
- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries.
### CI
- Run `cargo check` with musl libc.
- concourse: Install devpi in a virtual environment.
- Remove [mergeable](https://mergeable.us/) configuration.
### Documentation
- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently.
- CONTRIBUTING: document how conventional commits interact with squash merges.
### Refactor
- Rename `MimeMessage.header` into `MimeMessage.headers`.
- Derive `Default` trait for `WebxdcManifest`.
### Tests
- Regression test for case-sensitive comparison of gossip header to contact address.
- Multiple new group consistency tests in Rust.
- python: Replace legacy `tmpdir` fixture with `tmp_path`.
## [1.116.0] - 2023-06-05
### API-Changes
- Add `dc_jsonrpc_blocking_call()`.
### Changes
- Generate OpenRPC definitions for JSON-RPC.
- Add more context to message loading errors.
### Fixes
- Build deltachat-node prebuilds on Debian 10.
### Documentation
- Document release process in `RELEASE.md`.
- Add contributing guidelines `CONTRIBUTING.md`.
- Update instructions for python devenv.
- python: Document pytest fixtures.
### Tests
- python: Make `test_mdn_asymmetric` less flaky.
- Make `test_group_with_removed_message_id` less flaky.
- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)).
### Build system
- git-cliff: Changelog generation improvements.
- `set_core_version.py`: Expect release date in the changelog.
### CI
- Require Python 3.8 for deltachat-rpc-client.
- mergeable: Allow PR titles to start with "ci" and "build".
- Remove incorrect comment.
- dependabot: Use `chore` prefix for dependency updates.
- Remove broken `node-delete-preview.yml` workflow.
- Add top comments to GH Actions workflows.
- Run node.js lint on Windows.
- Update clippy to 1.70.0.
### Miscellaneous Tasks
- Remove release.toml.
- gitattributes: Configure LF line endings for JavaScript files.
- Update dependencies
## [1.112.10] - 2023-06-01
### Fixes
- Disable `fetch_existing_msgs` setting by default.
- Update `h2` to fix RUSTSEC-2023-0034.
## [1.115.0] - 2023-05-12
### JSON-RPC API Changes
- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)).
- Add API to get reactions outside the message snapshot.
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`.
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`.
- `JSONRPCReactions.reactions` is now a `Vec<JSONRPCReaction>` with unique reactions and their count, sorted in descending order.
- `Event`: `context_id` property is now called `contextId`.
- Expand `MessageSearchResult`:
- Always include `chat_name`(not an option anymore).
- Add `author_id`, `chat_type`, `chat_color`, `is_chat_protected`, `is_chat_contact_request`, `is_chat_archived`.
- `author_name` now contains the overridden sender name.
- `ChatListItemFetchResult` gets new properties: `summary_preview_image`, `last_message_type` and `last_message_id`
- New `MessageReadReceipt` type and `get_message_read_receipts(account_id, message_id)` jsonrpc method.
### API Changes
- New rust API `send_webxdc_status_update_struct` to send a `StatusUpdateItem`.
- Add `get_msg_read_receipts(context, msg_id)` - get the contacts that send read receipts for a message.
### Features / Changes
- Build deltachat-rpc-server releases for x86\_64 macOS.
- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)).
- Improve SMTP logging.
- Do not cut incoming text if "bot" config is set.
### Fixes
- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)).
- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)).
- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)).
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/pull/4390)).
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/deltachat/deltachat-core-rust/pull/4377)).
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)).
### Refactor
- Iterate over `msg_ids` without .iter().
## [1.112.9] - 2023-05-12
### Fixes
- Fetch at most 100 existing messages even if EXISTS was not received.
- Delete `smtp` rows when message sending is cancelled.
### Changes
- Improve SMTP logging.
## [1.114.0] - 2023-04-24
### Changes
- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events.
This better corresponds to JSON-RPC 2.0 server-client distinction
and is expected to simplify writing new bindings
because dispatching events can be done on higher level.
- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events.
### Fixes
- JSON-RPC: do not print to stdout on failure to find an account.
## [1.113.0] - 2023-04-18
### Added
- New JSON-RPC API `can_send()`.
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
These APIs can be used by bots to get all unprocessed messages
in the order of their arrival and wait for them without relying on events.
- New Python bindings API `Account.wait_next_incoming_message()`.
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
### Changes
- Increase MSRV to 1.65.0. #4236
- Remove upper limit on the attachment size. #4253
- Update rPGP to 0.10.1. #4236
- Compress HTML emails stored in the `mime_headers` column of the database.
- Strip BIDI characters in system messages, files, group names and contact names. #3479
- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`.
- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF.
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
in favor of `get_next_msgs()` and `wait_next_msgs()`.
- Remove metadata from avatars and JPEG images before sending. #4037
- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037
### Fixes
- Don't let blocking be bypassed using groups. #4316
- Show a warning if quota list is empty. #4261
- Do not reset status on other devices when sending signed reaction messages. #3692
- Update `accounts.toml` atomically.
- Fix python bindings README documentation on installing the bindings from source.
- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934
## [1.112.8] - 2023-04-20
### Changes
- Add `get_http_response` JSON-RPC API.
- Add C API to get HTTP responses.
## [1.112.7] - 2023-04-17
### Fixes
- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses.
## [1.112.6] - 2023-04-04
### Changes
- Add a device message after backup transfer #4301
### Fixed
- Updated `iroh` from 0.4.0 to 0.4.1 to fix transfer of large accounts with many blob files.
## [1.112.5] - 2023-04-02
### Fixes
- Run SQL database migrations after receiving a backup from the network. #4287
## [1.112.4] - 2023-03-31
### Fixes
- Fix call to `auditwheel` in `scripts/run_all.sh`.
## [1.112.3] - 2023-03-30
### Fixes
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
## [1.112.2] - 2023-03-30
### Changes
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
### Fixes
- Do not return media from trashed messages in the "All media" view. #4247
## [1.112.1] - 2023-03-27
### Changes
- Add support for `--version` argument to `deltachat-rpc-server`. #4224
It can be used to check the installed version without starting the server.
### Fixes
- deltachat-rpc-client: fix bug in `Chat.send_message()`: invalid `MessageData` field `quotedMsg` instead of `quotedMsgId`
- `receive_imf`: Mark special messages as seen. Exactly: delivery reports, webxdc status updates. #4230
## [1.112.0] - 2023-03-23
## [Unreleased]
### Changes
- "full message view" not needed because of footers that go to contact status #4151
- Pick up system's light/dark mode in generated message HTML #4150
- Support non-persistent configuration with DELTACHAT_* env
- Print deltachat-repl errors with causes. #4166
- Increase MSRV to 1.64. #4167
- Core takes care of stopping and re-starting IO itself where needed,
e.g. during backup creation.
It is no longer needed to call `dc_stop_io()`.
`dc_start_io()` can now be called at any time without harm. #4138
- Pick up system's light/dark mode in generated message HTML. #4150
- More accurate `maybe_add_bcc_self` device message text. #4175
- "Full message view" not needed because of footers that go to contact status. #4151
- Support non-persistent configuration with `DELTACHAT_*` env. #4154
- Print deltachat-repl errors with causes. #4166
e.g. during backup creation. It is no longer needed to call
dc_stop_io(). dc_start_io() can now be called at any time without
harm. #4138
- More accurate maybe_add_bcc_self device message text #4175
### Fixes
- Fix segmentation fault if `dc_context_unref()` is called during
@@ -499,11 +23,6 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- Do not emit "Failed to run incremental vacuum" warnings on success. #4160
- Ability to send backup over network and QR code to setup second device #4007
- Disable buffering during STARTTLS setup. #4190
- Add `DC_EVENT_IMAP_INBOX_IDLE` event to wait until the account
is ready for testing.
It is used to fix race condition between fetching
existing messages and starting the test. #4208
## [1.111.0] - 2023-03-05
@@ -634,6 +153,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- Do not treat invalid email addresses as an exception #3942
- Add timeouts to HTTP requests #3948
## 1.105.0
### Changes
@@ -719,6 +239,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- Disable read timeout during IMAP IDLE #3826
- Bots automatically accept mailing lists #3831
## 1.102.0
### Changes
@@ -2793,26 +2314,5 @@ For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...HEAD
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
[1.112.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.1...v1.112.2
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
[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

View File

@@ -1,92 +0,0 @@
# Contributing guidelines
## Reporting bugs
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
If the bug you found is specific to
[Android](https://github.com/deltachat/deltachat-android/issues),
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
report it to the corresponding repository.
## Proposing features
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
## Contributing code
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
If you have write access to the repository,
push a branch named `<username>/<feature>`
so it is clear who is responsible for the branch,
and open a PR proposing to merge the change.
Otherwise fork the repository and create a branch in your fork.
You can find the list of good first issues
and a link to this guide
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
### Coding conventions
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
- `test`: Test changes and improvements to the testing framework.
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
If you intend to squash merge the PR from the web interface,
make sure the PR title follows the conventional commits notation
as it will end up being a commit title.
Otherwise make sure each commit title follows the conventional commit notation.
#### Breaking Changes
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
Alternatively, breaking changes can go into the commit description, e.g.:
```
fix: Fix race condition and db corruption when a message was received during backup
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
```
#### Multiple Changes in one PR
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
[Clippy]: https://doc.rust-lang.org/clippy/
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Reviewing
Once a PR has an approval and passes CI, it can be merged.
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
This is to ensure that PRs are merged as intended by the author,
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
If you do not have access to the repository and created a PR from a fork,
ask the maintainers to merge the PR and say how it should be merged.
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).

2526
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE

View File

@@ -167,8 +167,8 @@ Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js**
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,6 @@ typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
typedef struct _dc_http_response dc_http_response_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
@@ -73,6 +72,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
*
* The example above uses "pthreads",
* however, you can also use anything else for thread handling.
* All deltachat-core functions, unless stated otherwise, are thread-safe.
*
* Now you can **configure the context:**
*
@@ -142,72 +142,6 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
* ~~~
*
*
* ## Thread safety
*
* All deltachat-core functions, unless stated otherwise, are thread-safe.
* In particular, it is safe to pass the same dc_context_t pointer
* to multiple functions running concurrently in different threads.
*
* All the functions are guaranteed not to use the reference passed to them
* after returning. If the function spawns a long-running process,
* such as dc_configure() or dc_imex(), it will ensure that the objects
* passed to them are not deallocated as long as they are needed.
* For example, it is safe to call dc_imex(context, ...) and
* call dc_context_unref(context) immediately after return from dc_imex().
* It is however **not safe** to call dc_context_unref(context) concurrently
* until dc_imex() returns, because dc_imex() may have not increased
* the reference count of dc_context_t yet.
*
* This means that the context may be still in use after
* dc_context_unref() call.
* For example, it is possible to start the import/export process,
* call dc_context_unref(context) immediately after
* and observe #DC_EVENT_IMEX_PROGRESS events via the event emitter.
* Once dc_get_next_event() returns NULL,
* it is safe to terminate the application.
*
* It is recommended to create dc_context_t in the main thread
* and only call dc_context_unref() once other threads that may use it,
* such as the event loop thread, are terminated.
* Common mistake is to use dc_context_unref() as a way
* to cause dc_get_next_event() return NULL and terminate event loop this way.
* If event loop thread is inside a function taking dc_context_t
* as an argument at the moment dc_context_unref() is called on the main thread,
* the behavior is undefined.
*
* Recommended way to safely terminate event loop
* and shutdown the application is
* to use a boolean variable
* indicating that the event loop should stop
* and check it in the event loop thread
* every time before calling dc_get_next_event().
* To terminate the event loop, main thread should:
* 1. Notify background threads,
* such as event loop (blocking in dc_get_next_event())
* and message processing loop (blocking in dc_wait_next_msgs()),
* that they should terminate by atomically setting the
* boolean flag in the memory
* shared between the main thread and background loop threads.
* 2. Call dc_stop_io() or dc_accounts_stop_io(), depending
* on whether a single account or account manager is used.
* Stopping I/O is guaranteed to emit at least one event
* and interrupt the event loop even if it was blocked on dc_get_next_event().
* Stopping I/O is guaranteed to interrupt a single dc_wait_next_msgs().
* 3. Wait until the event loop thread notices the flag,
* exits the event loop and terminates.
* 4. Call dc_context_unref() or dc_accounts_unref().
* 5. Keep calling dc_get_next_event() in a loop until it returns NULL,
* indicating that the contexts are deallocated.
* 6. Terminate the application.
*
* When using C API via FFI in runtimes that use automatic memory management,
* such as CPython, JVM or Node.js, take care to ensure the correct
* shutdown order and avoid calling dc_context_unref() or dc_accounts_unref()
* on the objects still in use in other threads,
* e.g. by keeping a reference to the wrapper object.
* The details depend on the runtime being used.
*
*
* ## Class reference
*
* For a class reference, see the "Classes" link atop.
@@ -301,19 +235,6 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Changes the passphrase on the open database.
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
* It is impossible to encrypt unencrypted database with this method and vice versa.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The new passphrase.
* @return 1 on success, 0 on error.
*/
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
/**
* Returns 1 if database is open.
*
@@ -433,19 +354,17 @@ char* dc_get_blobdir (const dc_context_t* context);
* 0=watch all folders normally (default)
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only,
* show direct replies to chats only (default),
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts (default).
* also show mails of unconfirmed contacts.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Curve25519 keypair
* DC_KEY_GEN_RSA4096 (3)=
* generate RSA 4096 keypair
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -476,19 +395,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages,
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
* and does not cut large incoming text messages.
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
* The value is updated automatically
* when dc_markseen_msgs() is called,
* but the bot can also set it manually if it processed
* the message but does not want to mark it as seen.
* For most bots calling `dc_markseen_msgs()` is the
* recommended way to update this value
* even for self-sent messages.
* adds Auto-Submitted header to outgoing messages
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -1175,24 +1083,6 @@ 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.
*
@@ -1354,20 +1244,6 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
/**
* Returns a list of similar chats.
*
* @warning This is an experimental API which may change or be removed in the future.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat for which to find similar chats.
* @return The list of similar chats.
* On errors, NULL is returned.
* Must be freed using dc_chatlist_unref() when no longer used.
*/
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
/**
* Estimate the number of messages that will be deleted
@@ -1407,56 +1283,6 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
/**
* Returns the message IDs of all messages of any chat
* with a database ID higher than `last_msg_id` config value.
*
* This function is intended for use by bots.
* Self-sent messages, device messages,
* messages from contact requests
* and muted chats are included,
* but messages from explicitly blocked contacts
* and chats are ignored.
*
* This function may be called as a part of event loop
* triggered by DC_EVENT_INCOMING_MSG if you are only interested
* in the incoming messages.
* Otherwise use a separate message processing loop
* calling dc_wait_next_msgs() in a separate thread.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
* On errors, the list is empty. NULL is never returned.
*/
dc_array_t* dc_get_next_msgs (dc_context_t* context);
/**
* Waits for notification of new messages
* and returns an array of new message IDs.
* See the documentation for dc_get_next_msgs()
* for the details of return value.
*
* This function waits for internal notification of
* a new message in the database and returns afterwards.
* Notification is also sent when I/O is started
* to allow processing new messages
* and when I/O is stopped using dc_stop_io() or dc_accounts_stop_io()
* to allow for manual interruption of the message processing loop.
* The function may return an empty array if there are
* no messages after notification,
* which may happen on start or if the message is quickly deleted
* after adding it to the database.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
* On errors, the list is empty. NULL is never returned.
*/
dc_array_t* dc_wait_next_msgs (dc_context_t* context);
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
@@ -2056,11 +1882,6 @@ int dc_resend_msgs (dc_context_t* context, const uint3
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* This function updates last_msg_id configuration value
* to the maximum of the current value and IDs passed to this function.
* Bots which mark messages as seen can rely on this side effect
* to avoid updating last_msg_id value manually.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*
* @memberof dc_context_t
@@ -2302,7 +2123,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
*
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
* If `param1` is a filename, import the private key from the file and make it the default.
*
* While dc_imex() returns immediately, the started job may take a while,
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
@@ -2895,12 +2715,6 @@ void dc_backup_provider_wait (dc_backup_provider_t* backup_provider);
/**
* Frees a dc_backup_provider_t object.
*
* If the provider has not yet finished, as indicated by
* dc_backup_provider_wait() or the #DC_EVENT_IMEX_PROGRESS event with value
* of 0 (failed) or 1000 (succeeded), this will also abort any in-progress
* transfer. If this aborts the provider a #DC_EVENT_IMEX_PROGRESS event with
* value 0 (failed) will be emitted.
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
@@ -4026,17 +3840,16 @@ char* dc_msg_get_text (const dc_msg_t* msg);
*/
char* dc_msg_get_subject (const dc_msg_t* msg);
/**
* Find out full path of the file associated with a message.
* Find out full path, file name and extension of the file associated with a
* message.
*
* Typically files are associated with images, videos, audios, documents.
* Plain text messages do not have a file.
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The full path (with file name and extension) of the file associated with the message.
* @return The full path, the file name, and the extension of the file associated with the message.
* If there is no file associated with the message, an empty string is returned.
* NULL is never returned and the returned value must be released using dc_str_unref().
*/
@@ -4044,13 +3857,14 @@ char* dc_msg_get_file (const dc_msg_t* msg);
/**
* Get an original attachment filename, with extension but without the path. To get the full path,
* use dc_msg_get_file().
* Get a base file name without the path. The base file name includes the extension; the path
* is not returned. To get the full path, use dc_msg_get_file().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The attachment filename. If there is no file associated with the message, an empty string
* is returned. The returned value must be released using dc_str_unref().
* @return The base file name plus the extension without part. If there is no file
* associated with the message, an empty string is returned. The returned
* value must be released using dc_str_unref().
*/
char* dc_msg_get_filename (const dc_msg_t* msg);
@@ -5177,72 +4991,6 @@ int dc_provider_get_status (const dc_provider_t* prov
void dc_provider_unref (dc_provider_t* provider);
/**
* Return an HTTP(S) GET response.
* This function can be used to download remote content for HTML emails.
*
* @memberof dc_context_t
* @param context The context object to take proxy settings from.
* @param url HTTP or HTTPS URL.
* @return The response must be released using dc_http_response_unref() after usage.
* NULL is returned on errors.
*/
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
/**
* @class dc_http_response_t
*
* An object containing an HTTP(S) GET response.
* Created by dc_get_http_response().
*/
/**
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
/**
* Returns HTTP response encoding, e.g. "utf-8".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_encoding (const dc_http_response_t* response);
/**
* Returns HTTP response contents.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
*/
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
/**
* Returns HTTP response content size.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob size.
*/
size_t dc_http_response_get_size (const dc_http_response_t* response);
/**
* Free an HTTP response object.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
*/
void dc_http_response_unref (const dc_http_response_t* response);
/**
* @class dc_lot_t
*
@@ -5720,6 +5468,7 @@ void dc_reactions_unref (dc_reactions_t* reactions);
*/
/**
* @class dc_jsonrpc_instance_t
*
@@ -5768,18 +5517,6 @@ void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* req
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Make a JSON-RPC call and return a response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
*
@@ -5968,14 +5705,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
/**
* Emitted before going into IDLE on the Inbox folder.
*
* @param data1 0
* @param data2 0
*/
#define DC_EVENT_IMAP_INBOX_IDLE 106
/**
* Emitted when a new blob file was successfully written
*
@@ -6127,15 +5856,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSG_READ 2015
/**
* A single message is deleted.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/
#define DC_EVENT_MSG_DELETED 2016
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
@@ -6314,7 +6034,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
#define DC_KEY_GEN_RSA4096 3
/**
@@ -7271,16 +6990,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
#define DC_STR_BACKUP_TRANSFER_QR 162
/// "Account transferred to your second device."
///
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/**
* @}
*/

View File

@@ -31,12 +31,11 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial};
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use num_traits::{FromPrimitive, ToPrimitive};
@@ -161,30 +160,11 @@ pub unsafe extern "C" fn dc_context_open(
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.open(passphrase))
.context("dc_context_open() failed")
.log_err(ctx)
.log_err(ctx, "dc_context_open() failed")
.map(|b| b as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_change_passphrase(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_change_passphrase()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.change_passphrase(passphrase))
.context("dc_context_change_passphrase() failed")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -236,18 +216,16 @@ pub unsafe extern "C" fn dc_set_config(
if key.starts_with("ui.") {
ctx.set_ui_config(&key, value.as_deref())
.await
.with_context(|| format!("dc_set_config failed: Can't set {key} to {value:?}"))
.log_err(ctx)
.with_context(|| format!("Can't set {key} to {value:?}"))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int
} else {
match config::Config::from_str(&key) {
Ok(key) => ctx
.set_config(key, value.as_deref())
.await
.with_context(|| {
format!("dc_set_config() failed: Can't set {key} to {value:?}")
})
.log_err(ctx)
.with_context(|| format!("Can't set {key} to {value:?}"))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int,
Err(_) => {
warn!(ctx, "dc_set_config(): invalid key");
@@ -275,8 +253,7 @@ pub unsafe extern "C" fn dc_get_config(
if key.starts_with("ui.") {
ctx.get_ui_config(&key)
.await
.context("Can't get ui-config")
.log_err(ctx)
.log_err(ctx, "Can't get ui-config")
.unwrap_or_default()
.unwrap_or_default()
.strdup()
@@ -285,8 +262,7 @@ pub unsafe extern "C" fn dc_get_config(
Ok(key) => ctx
.get_config(key)
.await
.context("Can't get config")
.log_err(ctx)
.log_err(ctx, "Can't get config")
.unwrap_or_default()
.unwrap_or_default()
.strdup(),
@@ -438,8 +414,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
block_on(async move {
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
.await
.context("dc_get_oauth2_url failed")
.log_err(ctx)
.log_err(ctx, "dc_get_oauth2_url failed")
{
Ok(Some(res)) => res.strdup(),
Ok(None) | Err(_) => ptr::null_mut(),
@@ -448,12 +423,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
}
fn spawn_configure(ctx: Context) {
spawn(async move {
ctx.configure()
.await
.context("Configure failed")
.log_err(&ctx)
});
spawn(async move { ctx.configure().await.log_err(&ctx, "Configure failed") });
}
#[no_mangle]
@@ -478,8 +448,7 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
block_on(async move {
ctx.is_configured()
.await
.context("failed to get configured state")
.log_err(ctx)
.log_err(ctx, "failed to get configured state")
.unwrap_or_default() as libc::c_int
})
}
@@ -531,7 +500,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::SmtpMessageSent(_) => 103,
EventType::ImapMessageDeleted(_) => 104,
EventType::ImapMessageMoved(_) => 105,
EventType::ImapInboxIdle => 106,
EventType::NewBlobFile(_) => 150,
EventType::DeletedBlobFile(_) => 151,
EventType::Warning(_) => 300,
@@ -545,7 +513,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
EventType::MsgRead { .. } => 2015,
EventType::MsgDeleted { .. } => 2016,
EventType::ChatModified(_) => 2020,
EventType::ChatEphemeralTimerModified { .. } => 2021,
EventType::ContactsChanged(_) => 2030,
@@ -577,7 +544,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SmtpMessageSent(_)
| EventType::ImapMessageDeleted(_)
| EventType::ImapMessageMoved(_)
| EventType::ImapInboxIdle
| EventType::NewBlobFile(_)
| EventType::DeletedBlobFile(_)
| EventType::Warning(_)
@@ -593,7 +559,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::MsgDelivered { chat_id, .. }
| EventType::MsgFailed { chat_id, .. }
| EventType::MsgRead { chat_id, .. }
| EventType::MsgDeleted { chat_id, .. }
| EventType::ChatModified(chat_id)
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
@@ -629,7 +594,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SmtpMessageSent(_)
| EventType::ImapMessageDeleted(_)
| EventType::ImapMessageMoved(_)
| EventType::ImapInboxIdle
| EventType::NewBlobFile(_)
| EventType::DeletedBlobFile(_)
| EventType::Warning(_)
@@ -651,8 +615,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
| EventType::MsgRead { msg_id, .. }
| EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
| EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::SecurejoinInviterProgress { progress, .. }
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
@@ -690,12 +653,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::ImapInboxIdle
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
| EventType::MsgFailed { .. }
| EventType::MsgRead { .. }
| EventType::MsgDeleted { .. }
| EventType::ChatModified(_)
| EventType::ContactsChanged(_)
| EventType::LocationChanged(_)
@@ -825,8 +786,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
Ok::<_, anyhow::Error>(1)
})
.context("Failed to save keypair")
.log_err(ctx)
.log_err(ctx, "Failed to save keypair")
.unwrap_or(0)
}
@@ -853,8 +813,7 @@ pub unsafe extern "C" fn dc_get_chatlist(
block_on(async move {
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
.await
.context("Failed to get chatlist")
.log_err(ctx)
.log_err(ctx, "Failed to get chatlist")
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
@@ -879,8 +838,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
block_on(async move {
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
.await
.context("Failed to create chat from contact_id")
.log_err(ctx)
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -900,8 +858,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
block_on(async move {
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
.await
.context("Failed to get chat for contact_id")
.log_err(ctx)
.log_err(ctx, "Failed to get chat for contact_id")
.unwrap_or_default() // unwraps the Result
.map(|id| id.to_u32())
.unwrap_or(0) // unwraps the Option
@@ -1043,8 +1000,7 @@ pub unsafe extern "C" fn dc_get_msg_reactions(
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.context("failed dc_get_msg_reactions() call")
.log_err(ctx)
.log_err(ctx, "failed dc_get_msg_reactions() call")
{
reactions
} else {
@@ -1072,8 +1028,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
&to_string_lossy(json),
&to_string_lossy(descr),
))
.context("Failed to send webxdc update")
.log_err(ctx)
.log_err(ctx, "Failed to send webxdc update")
.is_ok() as libc::c_int
}
@@ -1097,32 +1052,6 @@ 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,
@@ -1286,30 +1215,6 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_similar_chatlist(
context: *mut dc_context_t,
chat_id: u32,
) -> *mut dc_chatlist_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
return ptr::null_mut();
}
let ctx = &*context;
let chat_id = ChatId::new(chat_id);
match block_on(chat_id.get_similar_chatlist(ctx))
.context("failed to get similar chatlist")
.log_err(ctx)
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
context: *mut dc_context_t,
@@ -1342,8 +1247,7 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
let arr = dc_array_t::from(
ctx.get_fresh_msgs()
.await
.context("Failed to get fresh messages")
.log_err(ctx)
.log_err(ctx, "Failed to get fresh messages")
.unwrap_or_default()
.iter()
.map(|msg_id| msg_id.to_u32())
@@ -1353,50 +1257,6 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_next_msgs(context: *mut dc_context_t) -> *mut dc_array::dc_array_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_next_msgs()");
return ptr::null_mut();
}
let ctx = &*context;
let msg_ids = block_on(ctx.get_next_msgs())
.context("failed to get next messages")
.log_err(ctx)
.unwrap_or_default();
let arr = dc_array_t::from(
msg_ids
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
}
#[no_mangle]
pub unsafe extern "C" fn dc_wait_next_msgs(
context: *mut dc_context_t,
) -> *mut dc_array::dc_array_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_wait_next_msgs()");
return ptr::null_mut();
}
let ctx = &*context;
let msg_ids = block_on(ctx.wait_next_msgs())
.context("failed to wait for next messages")
.log_err(ctx)
.unwrap_or_default();
let arr = dc_array_t::from(
msg_ids
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
}
#[no_mangle]
pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id: u32) {
if context.is_null() {
@@ -1408,8 +1268,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
block_on(async move {
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
.await
.context("Failed marknoticed chat")
.log_err(ctx)
.log_err(ctx, "Failed marknoticed chat")
.unwrap_or(())
})
}
@@ -1551,8 +1410,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
ChatId::new(chat_id)
.set_visibility(ctx, visibility)
.await
.context("Failed setting chat visibility")
.log_err(ctx)
.log_err(ctx, "Failed setting chat visibility")
.unwrap_or(())
})
}
@@ -1565,10 +1423,12 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
}
let ctx = &*context;
block_on(ChatId::new(chat_id).delete(ctx))
.context("Failed chat delete")
.log_err(ctx)
.ok();
block_on(async move {
ChatId::new(chat_id)
.delete(ctx)
.await
.ok_or_log_msg(ctx, "Failed chat delete");
})
}
#[no_mangle]
@@ -1583,9 +1443,7 @@ pub unsafe extern "C" fn dc_block_chat(context: *mut dc_context_t, chat_id: u32)
ChatId::new(chat_id)
.block(ctx)
.await
.context("Failed chat block")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Failed chat block");
})
}
@@ -1601,9 +1459,7 @@ pub unsafe extern "C" fn dc_accept_chat(context: *mut dc_context_t, chat_id: u32
ChatId::new(chat_id)
.accept(ctx)
.await
.context("Failed chat accept")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Failed chat accept");
})
}
@@ -1701,8 +1557,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
block_on(async move {
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
.await
.context("Failed to create group chat")
.log_err(ctx)
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -1716,8 +1571,7 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
}
let ctx = &*context;
block_on(chat::create_broadcast_list(ctx))
.context("Failed to create broadcast list")
.log_err(ctx)
.log_err(ctx, "Failed to create broadcast list")
.map(|id| id.to_u32())
.unwrap_or(0)
}
@@ -1739,8 +1593,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
ChatId::new(chat_id),
ContactId::new(contact_id),
))
.context("is_contact_in_chat failed")
.log_err(ctx)
.log_err(ctx, "is_contact_in_chat failed")
.unwrap_or_default() as libc::c_int
}
@@ -1761,8 +1614,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
ChatId::new(chat_id),
ContactId::new(contact_id),
))
.context("Failed to add contact")
.log_err(ctx)
.log_err(ctx, "Failed to add contact")
.is_ok() as libc::c_int
}
@@ -1783,8 +1635,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
ChatId::new(chat_id),
ContactId::new(contact_id),
))
.context("Failed to remove contact")
.log_err(ctx)
.log_err(ctx, "Failed to remove contact")
.is_ok() as libc::c_int
}
@@ -1903,8 +1754,7 @@ pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
// ignored when ephemeral timer value is used to construct
// message headers.
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
.context("Failed to get ephemeral timer")
.log_err(ctx)
.log_err(ctx, "Failed to get ephemeral timer")
.unwrap_or_default()
.to_u32()
}
@@ -1925,8 +1775,7 @@ pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
ChatId::new(chat_id)
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
.await
.context("Failed to set ephemeral timer")
.log_err(ctx)
.log_err(ctx, "Failed to set ephemeral timer")
.is_ok() as libc::c_int
})
}
@@ -1941,10 +1790,13 @@ pub unsafe extern "C" fn dc_get_msg_info(
return "".strdup();
}
let ctx = &*context;
let msg_id = MsgId::new(msg_id);
block_on(msg_id.get_info(ctx))
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
block_on(async move {
message::get_msg_info(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
})
}
#[no_mangle]
@@ -1999,8 +1851,7 @@ pub unsafe extern "C" fn dc_delete_msgs(
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
block_on(message::delete_msgs(ctx, &msg_ids))
.context("failed dc_delete_msgs() call")
.log_err(ctx)
.log_err(ctx, "failed dc_delete_msgs() call")
.ok();
}
@@ -2064,8 +1915,7 @@ pub unsafe extern "C" fn dc_markseen_msgs(
let ctx = &*context;
block_on(message::markseen_msgs(ctx, msg_ids))
.context("failed dc_markseen_msgs() call")
.log_err(ctx)
.log_err(ctx, "failed dc_markseen_msgs() call")
.ok();
}
@@ -2107,8 +1957,7 @@ pub unsafe extern "C" fn dc_download_full_msg(context: *mut dc_context_t, msg_id
}
let ctx = &*context;
block_on(MsgId::new(msg_id).download_full(ctx))
.context("Failed to download message fully.")
.log_err(ctx)
.log_err(ctx, "Failed to download message fully.")
.ok();
}
@@ -2156,8 +2005,7 @@ pub unsafe extern "C" fn dc_create_contact(
let name = to_string_lossy(name);
block_on(Contact::create(ctx, &name, &to_string_lossy(addr)))
.context("Cannot create contact")
.log_err(ctx)
.log_err(ctx, "Cannot create contact")
.map(|id| id.to_u32())
.unwrap_or(0)
}
@@ -2234,8 +2082,7 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
Box::into_raw(Box::new(dc_array_t::from(
Contact::get_all_blocked(ctx)
.await
.context("Can't get blocked contacts")
.log_err(ctx)
.log_err(ctx, "Can't get blocked contacts")
.unwrap_or_default()
.iter()
.map(|id| id.to_u32())
@@ -2260,15 +2107,11 @@ pub unsafe extern "C" fn dc_block_contact(
if block == 0 {
Contact::unblock(ctx, contact_id)
.await
.context("Can't unblock contact")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Can't unblock contact");
} else {
Contact::block(ctx, contact_id)
.await
.context("Can't block contact")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Can't block contact");
}
});
}
@@ -2341,8 +2184,7 @@ fn spawn_imex(ctx: Context, what: imex::ImexMode, param1: String, passphrase: Op
spawn(async move {
imex::imex(&ctx, what, param1.as_ref(), passphrase)
.await
.context("IMEX failed")
.log_err(&ctx)
.log_err(&ctx, "IMEX failed")
});
}
@@ -2528,8 +2370,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
.await
.map(|chatid| chatid.to_u32())
.context("failed dc_join_securejoin() call")
.log_err(ctx)
.log_err(ctx, "failed dc_join_securejoin() call")
.unwrap_or_default()
})
}
@@ -2551,8 +2392,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
ChatId::new(chat_id),
seconds as i64,
))
.context("Failed dc_send_locations_to_chat()")
.log_err(ctx)
.log_err(ctx, "Failed dc_send_locations_to_chat()")
.ok();
}
@@ -2635,8 +2475,7 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
block_on(async move {
location::delete_all(ctx)
.await
.context("Failed to delete locations")
.log_err(ctx)
.log_err(ctx, "Failed to delete locations")
.ok()
});
}
@@ -2920,8 +2759,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
.list
.get_summary(ctx, index, maybe_chat)
.await
.context("get_summary failed")
.log_err(ctx)
.log_err(ctx, "get_summary failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
})
@@ -2949,8 +2787,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
msg_id,
None,
))
.context("get_summary2 failed")
.log_err(ctx)
.log_err(ctx, "get_summary2 failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
@@ -3133,8 +2970,7 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
let ffi_chat = &*chat;
let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.can_send(ctx))
.context("can_send failed")
.log_err(ctx)
.log_err(ctx, "can_send failed")
.unwrap_or_default() as libc::c_int
}
@@ -3369,7 +3205,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_text().strdup()
ffi_msg.message.get_text().unwrap_or_default().strdup()
}
#[no_mangle]
@@ -3579,8 +3415,7 @@ pub unsafe extern "C" fn dc_msg_get_summary(
let ctx = &*ffi_msg.context;
let summary = block_on(ffi_msg.message.get_summary(ctx, maybe_chat))
.context("dc_msg_get_summary failed")
.log_err(ctx)
.log_err(ctx, "dc_msg_get_summary failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
@@ -3598,8 +3433,7 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
let ctx = &*ffi_msg.context;
let summary = block_on(ffi_msg.message.get_summary(ctx, None))
.context("dc_msg_get_summarytext failed")
.log_err(ctx)
.log_err(ctx, "dc_msg_get_summarytext failed")
.unwrap_or_default();
match usize::try_from(approx_characters) {
Ok(chars) => summary.truncated_text(chars).strdup(),
@@ -3754,7 +3588,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
return;
}
let ffi_msg = &mut *msg;
ffi_msg.message.set_text(to_string_lossy(text))
ffi_msg.message.set_text(to_opt_string_lossy(text))
}
#[no_mangle]
@@ -3866,9 +3700,7 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
.message
.latefiling_mediasize(ctx, width, height, duration)
})
.context("Cannot set media size")
.log_err(ctx)
.ok();
.ok_or_log_msg(ctx, "Cannot set media size");
}
#[no_mangle]
@@ -3907,8 +3739,7 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
.message
.set_quote(&*ffi_msg.context, quote_msg)
.await
.context("failed to set quote")
.log_err(&*ffi_msg.context)
.log_err(&*ffi_msg.context, "failed to set quote")
.ok();
});
}
@@ -3939,8 +3770,7 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
.message
.quoted_message(context)
.await
.context("failed to get quoted message")
.log_err(context)
.log_err(context, "failed to get quoted message")
.unwrap_or(None)
});
@@ -3963,8 +3793,7 @@ pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_
.message
.parent(context)
.await
.context("failed to get parent message")
.log_err(context)
.log_err(context, "failed to get parent message")
.unwrap_or(None)
});
@@ -4155,8 +3984,7 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.is_verified(ctx))
.context("is_verified failed")
.log_err(ctx)
.log_err(ctx, "is_verified failed")
.unwrap_or_default() as libc::c_int
}
@@ -4171,8 +3999,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr(
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.get_verifier_addr(ctx))
.context("failed to get verifier for contact")
.log_err(ctx)
.log_err(ctx, "failed to get verifier for contact")
.unwrap_or_default()
.strdup()
}
@@ -4186,8 +4013,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx))
.context("failed to get verifier")
.log_err(ctx)
.log_err(ctx, "failed to get verifier")
.unwrap_or_default()
.unwrap_or_default();
@@ -4339,8 +4165,8 @@ pub unsafe extern "C" fn dc_backup_provider_new(
provider,
})
.map(|ffi_provider| Box::into_raw(Box::new(ffi_provider)))
.log_err(ctx, "BackupProvider failed")
.context("BackupProvider failed")
.log_err(ctx)
.set_last_error(ctx)
.unwrap_or(ptr::null_mut())
}
@@ -4356,8 +4182,8 @@ pub unsafe extern "C" fn dc_backup_provider_get_qr(
let ffi_provider = &*provider;
let ctx = &*ffi_provider.context;
deltachat::qr::format_backup(&ffi_provider.provider.qr())
.log_err(ctx, "BackupProvider get_qr failed")
.context("BackupProvider get_qr failed")
.log_err(ctx)
.set_last_error(ctx)
.unwrap_or_default()
.strdup()
@@ -4375,8 +4201,8 @@ pub unsafe extern "C" fn dc_backup_provider_get_qr_svg(
let ctx = &*ffi_provider.context;
let provider = &ffi_provider.provider;
block_on(generate_backup_qr(ctx, &provider.qr()))
.log_err(ctx, "BackupProvider get_qr_svg failed")
.context("BackupProvider get_qr_svg failed")
.log_err(ctx)
.set_last_error(ctx)
.unwrap_or_default()
.strdup()
@@ -4392,18 +4218,14 @@ pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provid
let ctx = &*ffi_provider.context;
let provider = &mut ffi_provider.provider;
block_on(provider)
.log_err(ctx, "Failed to await BackupProvider")
.context("Failed to await BackupProvider")
.log_err(ctx)
.set_last_error(ctx)
.ok();
}
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_unref(provider: *mut dc_backup_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_backup_provider_unref()");
return;
}
drop(Box::from_raw(provider));
}
@@ -4418,28 +4240,17 @@ pub unsafe extern "C" fn dc_receive_backup(
}
let ctx = &*context;
let qr_text = to_string_lossy(qr);
receive_backup(ctx.clone(), qr_text)
}
// Because this is a long-running operation make sure we own the Context. This stops a FFI
// user from deallocating it by calling unref on the object while we are using it.
fn receive_backup(ctx: Context, qr_text: String) -> libc::c_int {
let qr = match block_on(qr::check_qr(&ctx, &qr_text))
.context("Invalid QR code")
.log_err(&ctx)
.set_last_error(&ctx)
{
let qr = match block_on(qr::check_qr(ctx, &qr_text)).log_err(ctx, "Invalid QR code") {
Ok(qr) => qr,
Err(_) => return 0,
};
match block_on(imex::get_backup(&ctx, qr))
.context("Get backup failed")
.log_err(&ctx)
.set_last_error(&ctx)
{
Ok(_) => 1,
Err(_) => 0,
}
spawn(async move {
imex::get_backup(ctx, qr)
.await
.log_err(ctx, "Get backup failed")
.ok();
});
1
}
trait ResultExt<T, E> {
@@ -4486,8 +4297,8 @@ where
///
/// ```no_compile
/// some_dc_rust_api_call_returning_result()
/// .log_err(&context, "My API call failed")
/// .context("My API call failed")
/// .log_err(&context)
/// .set_last_error(&context)
/// .unwrap_or_default()
/// ```
@@ -4574,8 +4385,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.context("Can't get config")
.log_err(ctx)
.log_err(ctx, "Can't get config")
});
match socks5_enabled {
@@ -4638,96 +4448,6 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
// this may change once we start localizing string.
}
// dc_http_response_t
pub type dc_http_response_t = net::HttpResponse;
#[no_mangle]
pub unsafe extern "C" fn dc_get_http_response(
context: *const dc_context_t,
url: *const libc::c_char,
) -> *mut dc_http_response_t {
if context.is_null() || url.is_null() {
eprintln!("ignoring careless call to dc_get_http_response()");
return ptr::null_mut();
}
let context = &*context;
let url = to_string_lossy(url);
if let Ok(response) = block_on(read_url_blob(context, &url))
.context("read_url_blob")
.log_err(context)
{
Box::into_raw(Box::new(response))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_mimetype(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
return ptr::null_mut();
}
let response = &*response;
response.mimetype.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_encoding(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_encoding()");
return ptr::null_mut();
}
let response = &*response;
response.encoding.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_blob(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_blob()");
return ptr::null_mut();
}
let response = &*response;
let blob_len = response.blob.len();
let ptr = libc::malloc(blob_len);
libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
ptr as *mut libc::c_char
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_size(
response: *const dc_http_response_t,
) -> libc::size_t {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_size()");
return 0;
}
let response = &*response;
response.blob.len()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_unref()");
return;
}
drop(Box::from_raw(response));
}
// -- Accounts
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
@@ -5032,13 +4752,15 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use super::*;
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
event_thread: JoinHandle<Result<(), anyhow::Error>>,
}
#[no_mangle]
@@ -5051,12 +4773,28 @@ mod jsonrpc {
}
let account_manager = &*account_manager;
let events = block_on(account_manager.read()).get_event_emitter();
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let handle = RpcSession::new(request_handle.clone(), cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
let event_thread = spawn(async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
request_handle
.send_notification("event", Some(event))
.await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
});
let instance = dc_jsonrpc_instance_t {
receiver,
handle,
event_thread,
};
Box::into_raw(Box::new(instance))
}
@@ -5067,6 +4805,7 @@ mod jsonrpc {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
(*jsonrpc_instance).event_thread.abort();
drop(Box::from_raw(jsonrpc_instance));
}
@@ -5104,29 +4843,4 @@ mod jsonrpc {
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
method: *const libc::c_char,
params: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
match res {
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

@@ -1,4 +1,3 @@
openrpc/openrpc.json
accounts/
.cargo

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.122.0"
version = "1.111.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -15,26 +15,25 @@ required-features = ["webserver"]
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.11"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.6.0"
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.99"
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
futures = { version = "0.3.26" }
serde_json = "1.0.91"
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.29.1" }
tokio = { version = "1.25.0" }
sanitize-filename = "0.4"
walkdir = "2.3.3"
walkdir = "2.3.2"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.18", optional = true, features = ["ws"] }
axum = { version = "0.6.11", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
[features]

View File

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

View File

@@ -4,7 +4,6 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::message::get_msg_read_receipts;
use deltachat::qr::Qr;
use deltachat::{
chat::{
@@ -19,11 +18,13 @@ use deltachat::{
context::get_info,
ephemeral::Timer,
imex, location,
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
message::{
self, delete_msgs, get_msg_info, 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},
reaction::send_reaction,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
@@ -34,17 +35,17 @@ use tokio::sync::{watch, Mutex, RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
pub mod events;
pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::message::MessageData;
use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
@@ -142,11 +143,7 @@ impl CommandApi {
}
}
#[rpc(
all_positional,
ts_outdir = "typescript/generated",
openrpc_outdir = "openrpc"
)]
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
@@ -167,16 +164,6 @@ impl CommandApi {
get_info()
}
/// Get the next event.
async fn get_next_event(&self) -> Result<Event> {
let event_emitter = self.accounts.read().await.get_event_emitter();
event_emitter
.recv()
.await
.map(|event| event.into())
.context("event channel is closed")
}
// ---------------------------------------------
// Account Management
// ---------------------------------------------
@@ -218,6 +205,8 @@ impl CommandApi {
let context_option = self.accounts.read().await.get_account(id);
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
println!("account with id {id} doesn't exist anymore");
}
}
Ok(accounts)
@@ -464,49 +453,6 @@ impl CommandApi {
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
/// Gets messages to be processed by the bot and returns their IDs.
///
/// Only messages with database ID higher than `last_msg_id` config value
/// are returned. After processing the messages, the bot should
/// update `last_msg_id` by calling [`markseen_msgs`]
/// or manually updating the value to avoid getting already
/// processed messages.
///
/// [`markseen_msgs`]: Self::markseen_msgs
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids = ctx
.get_next_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect();
Ok(msg_ids)
}
/// Waits for messages to be processed by the bot and returns their IDs.
///
/// This function is similar to [`get_next_msgs`],
/// but waits for internal new message notification before returning.
/// New message notification is sent when new message is added to the database,
/// on initialization, when I/O is started and when I/O is stopped.
/// This allows bots to use `wait_next_msgs` in a loop to process
/// old messages after initialization and during the bot runtime.
/// To shutdown the bot, stopping I/O can be used to interrupt
/// pending or next `wait_next_msgs` call.
///
/// [`get_next_msgs`]: Self::get_next_msgs
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids = ctx
.wait_next_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect();
Ok(msg_ids)
}
/// Estimate the number of messages that will be deleted
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
/// This is typically used to show the estimated impact to the user
@@ -550,7 +496,7 @@ impl CommandApi {
list_flags: Option<u32>,
query_string: Option<String>,
query_contact_id: Option<u32>,
) -> Result<Vec<u32>> {
) -> Result<Vec<ChatListEntry>> {
let ctx = self.get_context(account_id).await?;
let list = Chatlist::try_load(
&ctx,
@@ -559,43 +505,32 @@ impl CommandApi {
query_contact_id.map(ContactId::new),
)
.await?;
let mut l: Vec<u32> = Vec::with_capacity(list.len());
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(list.get_chat_id(i)?.to_u32());
l.push(ChatListEntry(
list.get_chat_id(i)?.to_u32(),
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
));
}
Ok(l)
}
/// Returns chats similar to the given one.
///
/// Experimental API, subject to change without notice.
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let list = chat_id
.get_similar_chat_ids(&ctx)
.await?
.into_iter()
.map(|(chat_id, _metric)| chat_id.to_u32())
.collect();
Ok(list)
}
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
entries: Vec<u32>,
entries: Vec<ChatListEntry>,
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
// todo custom json deserializer for ChatListEntry?
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<u32, ChatListItemFetchResult> =
HashMap::with_capacity(entries.len());
for &entry in entries.iter() {
for entry in entries.iter() {
result.insert(
entry,
entry.0,
match get_chat_list_item_by_id(&ctx, entry).await {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry,
id: entry.0,
error: format!("{err:#}"),
},
},
@@ -914,7 +849,7 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
msg.set_text(Some(text));
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
@@ -1009,11 +944,6 @@ impl CommandApi {
/// Moreover, timer is started for incoming ephemeral messages.
/// This also happens for contact requests chats.
///
/// This function updates `last_msg_id` configuration value
/// to the maximum of the current value and IDs passed to this function.
/// Bots which mark messages as seen can rely on this side effect
/// to avoid updating `last_msg_id` value manually.
///
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1132,25 +1062,7 @@ impl CommandApi {
/// max. text returned by dc_msg_get_text() (about 30000 characters).
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).get_info(&ctx).await
}
/// Returns contacts that sent read receipts and the time of reading.
async fn get_message_read_receipts(
&self,
account_id: u32,
message_id: u32,
) -> Result<Vec<MessageReadReceipt>> {
let ctx = self.get_context(account_id).await?;
let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
.await?
.iter()
.map(|(contact_id, ts)| MessageReadReceipt {
contact_id: contact_id.to_u32(),
timestamp: *ts,
})
.collect();
Ok(receipts)
get_msg_info(&ctx, MsgId::new(message_id)).await
}
/// Asks the core to start downloading a message fully.
@@ -1170,17 +1082,17 @@ impl CommandApi {
}
/// Search messages containing the given query string.
/// Searching can be done globally (chat_id=None) or in a specified chat only (chat_id set).
/// Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
///
/// Global search results are typically displayed using dc_msg_get_summary(), chat
/// search results may just highlight the corresponding messages and present a
/// Global chat results are typically displayed using dc_msg_get_summary(), chat
/// search results may just hilite the corresponding messages and present a
/// prev/next button.
///
/// For the global search, the result is limited to 1000 messages,
/// this allows an incremental search done fast.
/// So, when getting exactly 1000 messages, the result actually may be truncated;
/// the UIs may display sth. like "1000+ messages found" in this case.
/// The chat search (if chat_id is set) is not limited.
/// For global search, result is limited to 1000 messages,
/// this allows incremental search done fast.
/// So, when getting exactly 1000 results, the result may be truncated;
/// the UIs may display sth. as "1000+ messages found" in this case.
/// Chat search (if a chat_id is set) is not limited.
async fn search_messages(
&self,
account_id: u32,
@@ -1353,7 +1265,7 @@ impl CommandApi {
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
let contact = Contact::get_by_id(&ctx, contact_id).await?;
let contact = Contact::load_from_db(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
@@ -1698,15 +1610,6 @@ impl CommandApi {
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
async fn get_http_response(&self, account_id: u32, url: String) -> Result<HttpResponse> {
let ctx = self.get_context(account_id).await?;
let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
Ok(response)
}
/// Forward messages to another chat.
///
/// All types of messages can be forwarded,
@@ -1724,20 +1627,6 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
/// Resend messages and make information available for newly added chat members.
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
/// Clients that already have the original message can still ignore the resent message as
/// they have tracked the state by dedicated updates.
///
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
///
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
chat::resend_msgs(&ctx, &message_ids).await
}
async fn send_sticker(
&self,
account_id: u32,
@@ -1770,21 +1659,6 @@ impl CommandApi {
Ok(message_id.to_u32())
}
/// Returns reactions to the message.
async fn get_message_reactions(
&self,
account_id: u32,
message_id: u32,
) -> Result<Option<JSONRPCReactions>> {
let ctx = self.get_context(account_id).await?;
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
if reactions.is_empty() {
Ok(None)
} else {
Ok(Some(reactions.into()))
}
}
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
@@ -1794,7 +1668,9 @@ impl CommandApi {
} else {
Viewtype::Text
});
message.set_text(data.text.unwrap_or_default());
if data.text.is_some() {
message.set_text(data.text);
}
if data.html.is_some() {
message.set_html(data.html);
}
@@ -1825,15 +1701,6 @@ impl CommandApi {
Ok(msg_id)
}
/// Checks if messages can be sent to a given chat.
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(&ctx, chat_id).await?;
let can_send = chat.can_send(&ctx).await?;
Ok(can_send)
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1975,7 +1842,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
msg.set_text(Some(text));
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
@@ -1998,7 +1865,9 @@ impl CommandApi {
} else {
Viewtype::Text
});
message.set_text(text.unwrap_or_default());
if text.is_some() {
message.set_text(text);
}
if let Some(file) = file {
message.set_file(file, None);
}
@@ -2042,7 +1911,9 @@ impl CommandApi {
} else {
Viewtype::Text
});
draft.set_text(text.unwrap_or_default());
if text.is_some() {
draft.set_text(text);
}
if let Some(file) = file {
draft.set_file(file, None);
}

View File

@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum Account {
#[serde(rename_all = "camelCase")]

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context as _, Result};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
@@ -13,7 +13,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct FullChat {
id: u32,
@@ -53,9 +53,7 @@ impl FullChat {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::get_by_id(context, *contact_id)
.await
.context("failed to load contact")?,
Contact::load_from_db(context, *contact_id).await?,
)
.await?,
)
@@ -74,9 +72,8 @@ impl FullChat {
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::get_by_id(context, *contact)
.await
.context("failed to load contact for was_seen_recently")?
Some(contact) => Contact::load_from_db(context, *contact)
.await?
.was_seen_recently(),
None => false,
}
@@ -92,7 +89,10 @@ impl FullChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
@@ -121,7 +121,7 @@ impl FullChat {
/// - can_send
///
/// used when you only need the basic metadata of a chat like type, name, profile picture
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct BasicChat {
id: u32,
@@ -155,7 +155,10 @@ impl BasicChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,
@@ -166,7 +169,7 @@ impl BasicChat {
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, TypeDef)]
pub enum MuteDuration {
NotMuted,
Forever,
@@ -191,7 +194,7 @@ impl MuteDuration {
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, TypeDef)]
#[serde(rename = "ChatVisibility")]
pub enum JSONRPCChatVisibility {
Normal,

View File

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

View File

@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename = "Contact", rename_all = "camelCase")]
pub struct ContactObject {
address: String,

View File

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

View File

@@ -2,7 +2,7 @@ use deltachat::location::Location;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename = "Location", rename_all = "camelCase")]
pub struct JsonrpcLocation {
pub location_id: u32,

View File

@@ -1,7 +1,7 @@
use anyhow::{Context as _, Result};
use anyhow::{anyhow, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
use deltachat::chat::ChatVisibility;
use deltachat::constants::Chattype;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
@@ -10,7 +10,8 @@ use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
@@ -18,14 +19,14 @@ use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject {
id: u32,
@@ -34,7 +35,7 @@ pub struct MessageObject {
quote: Option<MessageQuote>,
parent_id: Option<u32>,
text: String,
text: Option<String>,
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -85,7 +86,7 @@ pub struct MessageObject {
reactions: Option<JSONRPCReactions>,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(tag = "kind")]
enum MessageQuote {
JustText {
@@ -113,12 +114,8 @@ impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::get_by_id(context, message.get_from_id())
.await
.context("failed to load sender contact")?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
.await
.context("failed to load sender contact object")?;
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
let override_sender_name = message.get_override_sender_name();
@@ -135,9 +132,7 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::get_by_id(context, quote.get_from_id())
.await
.context("failed to load quote author contact")?;
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
Some(MessageQuote::WithMessage {
text: quoted_text,
message_id: quote.get_id().to_u32(),
@@ -165,9 +160,7 @@ impl MessageObject {
None
};
let reactions = get_msg_reactions(context, msg_id)
.await
.context("failed to load message reactions")?;
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = if reactions.is_empty() {
None
} else {
@@ -187,7 +180,7 @@ impl MessageObject {
state: message
.get_state()
.to_u32()
.context("state conversion to number failed")?,
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
error: message.error(),
timestamp: message.get_timestamp(),
@@ -210,7 +203,7 @@ impl MessageObject {
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.context("videochat type conversion to number failed")?,
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
),
None => None,
},
@@ -237,7 +230,7 @@ impl MessageObject {
}
}
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, Deserialize, TypeDef)]
#[serde(rename = "Viewtype")]
pub enum MessageViewtype {
Unknown,
@@ -313,12 +306,11 @@ impl From<MessageViewtype> for Viewtype {
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
pub enum DownloadState {
Done,
Available,
Failure,
Undecipherable,
InProgress,
}
@@ -328,13 +320,12 @@ impl From<download::DownloadState> for DownloadState {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
pub enum SystemMessageType {
Unknown,
GroupNameChanged,
@@ -389,7 +380,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo {
id: u32,
@@ -447,22 +438,14 @@ impl MessageNotificationInfo {
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageSearchResult {
id: u32,
author_profile_image: Option<String>,
/// if sender name if overridden it will show it as ~alias
author_name: String,
author_color: String,
author_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
chat_type: u32,
is_chat_protected: bool,
is_chat_contact_request: bool,
is_chat_archived: bool,
chat_name: Option<String>,
message: String,
timestamp: i64,
}
@@ -471,44 +454,30 @@ impl MessageSearchResult {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
let profile_image = match sender.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let chat_profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let author_name = if let Some(name) = message.get_override_sender_name() {
format!("~{name}")
} else {
sender.get_display_name().to_owned()
};
let chat_color = color_int_to_hex_string(chat.get_color(context).await?);
Ok(Self {
id: msg_id.to_u32(),
author_profile_image: profile_image,
author_name,
author_name: sender.get_display_name().to_owned(),
author_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_profile_image,
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text(),
chat_name: if chat.get_type() == Chattype::Single {
Some(chat.get_name().to_owned())
} else {
None
},
message: message.get_text().unwrap_or_default(),
timestamp: message.get_timestamp(),
})
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
pub enum JSONRPCMessageListItem {
Message {
@@ -534,7 +503,7 @@ impl From<ChatItem> for JSONRPCMessageListItem {
}
}
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Deserialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageData {
pub text: Option<String>,
@@ -545,10 +514,3 @@ pub struct MessageData {
pub override_sender_name: Option<String>,
pub quoted_message_id: Option<u32>,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageReadReceipt {
pub contact_id: u32,
pub timestamp: i64,
}

View File

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

View File

@@ -3,7 +3,7 @@ use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
pub before_login_hint: String,

View File

@@ -2,7 +2,7 @@ use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {

View File

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

View File

@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
use super::maybe_empty_string_to_option;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[derive(Serialize, TypeDef)]
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
pub struct WebxdcMessageInfo {
/// The name of the app.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -187,7 +187,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]",
DownloadState::Failure => " [⬇ Download failed]",
DownloadState::Undecipherable => " [⬇ Decryption failed]",
};
let temp2 = timestamp_to_str(msg.get_timestamp());
@@ -200,7 +199,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
if msg.has_location() { "📍" } else { "" },
&contact_name,
contact_id,
msgtext,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == ContactId::SELF {
""
@@ -564,7 +563,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await.log_err(&context).ok();
sql::housekeeping(&context).await.ok_or_log(&context);
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" {
@@ -806,30 +805,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await?;
println!("{} contacts", contacts.len());
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
if !similar_chats.is_empty() {
println!("Similar chats: ");
for (similar_chat_id, metric) in similar_chats {
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
println!(
"{} (#{}) {:.1}",
similar_chat.name,
similar_chat_id,
100.0 * metric
);
}
}
println!(
"Location streaming: {}",
"{} contacts\nLocation streaming: {}",
contacts.len(),
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
@@ -928,7 +912,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::File
});
msg.set_file(arg1, None);
msg.set_text(arg2.to_string());
if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string()));
}
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendhtml" => {
@@ -940,11 +926,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string()));
msg.set_text(if arg2.is_empty() {
msg.set_text(Some(if arg2.is_empty() {
path.file_name().unwrap().to_string_lossy().to_string()
} else {
arg2.to_string()
});
}));
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendsyncmsg" => match context.send_sync_msg().await? {
@@ -993,7 +979,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string());
draft.set_text(Some(arg1.to_string()));
sel_chat
.as_ref()
.unwrap()
@@ -1017,7 +1003,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(arg1.to_string());
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1104,7 +1090,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = id.get_info(&context).await?;
let res = message::get_msg_info(&context, id).await?;
println!("{res}");
}
"download" => {

View File

@@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

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

View File

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

View File

@@ -6,22 +6,8 @@ build-backend = "setuptools.build_meta"
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",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
"aiohttp",
"aiodns"
]
dynamic = [
"version"
@@ -35,9 +21,6 @@ deltachat_rpc_client = [
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.setuptools_scm]
root = ".."
[tool.black]
line-length = 120

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,6 @@ class EventType(str, Enum):
SMTP_MESSAGE_SENT = "SmtpMessageSent"
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
IMAP_INBOX_IDLE = "ImapInboxIdle"
NEW_BLOB_FILE = "NewBlobFile"
DELETED_BLOB_FILE = "DeletedBlobFile"
WARNING = "Warning"
@@ -45,7 +44,6 @@ class EventType(str, Enum):
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"

View File

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

View File

@@ -16,8 +16,9 @@ async def get_temp_credentials() -> dict:
# 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())
async with aiohttp.ClientSession() as session:
async with session.post(url, timeout=timeout) as response:
return json.loads(await response.text())
class ACFactory:
@@ -54,11 +55,6 @@ class ACFactory:
async def get_online_account(self) -> Account:
account = await self.new_configured_account()
await account.start_io()
while True:
event = await account.wait_for_event()
print(event)
if event.type == EventType.IMAP_INBOX_IDLE:
break
return account
async def get_online_accounts(self, num: int) -> List[Account]:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,64 +2,51 @@
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Only affects windows if using non-default allocator (and unmaintained).
"RUSTSEC-2021-0145",
]
[bans]
# Accept some duplicate versions, ideally we work towards this list
# becoming empty. Adding versions forces us to revisit this at least
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "ahash", version = "0.7.6" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling", version = "<0.14" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "env_logger", version = "<0.10" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "hermit-abi", version = "<0.3" },
{ name = "humantime", version = "<2.1" },
{ 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 = "nom", version = "<7.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand", version = "<0.8" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.37.21" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "socket2", version = "0.4.9" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "version_check", version = "<0.9" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
{ name = "windows_aarch64_msvc", version = "<0.48" },
{ name = "windows_i686_gnu", version = "<0.48" },
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows", 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" },
{ name = "windows-sys", version = "<0.45" },
{ name = "windows_x86_64_msvc", version = "<0.42" },
{ name = "windows_x86_64_gnu", version = "<0.42" },
{ name = "windows_i686_msvc", version = "<0.42" },
{ name = "windows_i686_gnu", version = "<0.42" },
{ name = "windows_aarch64_msvc", version = "<0.42" },
{ name = "unicode-xid", version = "<0.2.4" },
{ name = "syn", version = "<1.0" },
{ name = "quote", version = "<1.0" },
{ name = "proc-macro2", version = "<1.0" },
{ name = "portable-atomic", version = "<1.0" },
{ name = "spin", version = "<0.9.6" },
{ name = "convert_case", version = "0.4.0" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
]
@@ -91,5 +78,7 @@ license-files = [
github = [
"async-email",
"deltachat",
"n0-computer",
"quinn-rs",
"dignifiedquire",
]

1861
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,6 @@ module.exports = {
DC_EVENT_ERROR: 400,
DC_EVENT_ERROR_SELF_NOT_IN_GROUP: 410,
DC_EVENT_IMAP_CONNECTED: 102,
DC_EVENT_IMAP_INBOX_IDLE: 106,
DC_EVENT_IMAP_MESSAGE_DELETED: 104,
DC_EVENT_IMAP_MESSAGE_MOVED: 105,
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
@@ -48,7 +47,6 @@ module.exports = {
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
DC_EVENT_MSGS_NOTICED: 2008,
DC_EVENT_MSG_DELETED: 2016,
DC_EVENT_MSG_DELIVERED: 2010,
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
@@ -90,7 +88,6 @@ module.exports = {
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
DC_KEY_GEN_RSA4096: 3,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,
@@ -115,7 +112,6 @@ module.exports = {
DC_QR_ADDR: 320,
DC_QR_ASK_VERIFYCONTACT: 200,
DC_QR_ASK_VERIFYGROUP: 202,
DC_QR_BACKUP: 251,
DC_QR_ERROR: 400,
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
@@ -153,8 +149,6 @@ module.exports = {
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
DC_STR_BACKUP_TRANSFER_MSG_BODY: 163,
DC_STR_BACKUP_TRANSFER_QR: 162,
DC_STR_BAD_TIME_MSG_BODY: 85,
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,

View File

@@ -8,7 +8,6 @@ module.exports = {
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
106: 'DC_EVENT_IMAP_INBOX_IDLE',
150: 'DC_EVENT_NEW_BLOB_FILE',
151: 'DC_EVENT_DELETED_BLOB_FILE',
300: 'DC_EVENT_WARNING',
@@ -22,7 +21,6 @@ module.exports = {
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
2016: 'DC_EVENT_MSG_DELETED',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',

View File

@@ -37,7 +37,6 @@ export enum C {
DC_EVENT_ERROR = 400,
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
DC_EVENT_IMAP_CONNECTED = 102,
DC_EVENT_IMAP_INBOX_IDLE = 106,
DC_EVENT_IMAP_MESSAGE_DELETED = 104,
DC_EVENT_IMAP_MESSAGE_MOVED = 105,
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
@@ -48,7 +47,6 @@ export enum C {
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
DC_EVENT_MSGS_NOTICED = 2008,
DC_EVENT_MSG_DELETED = 2016,
DC_EVENT_MSG_DELIVERED = 2010,
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
@@ -90,7 +88,6 @@ export enum C {
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_KEY_GEN_RSA4096 = 3,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
@@ -115,7 +112,6 @@ export enum C {
DC_QR_ADDR = 320,
DC_QR_ASK_VERIFYCONTACT = 200,
DC_QR_ASK_VERIFYGROUP = 202,
DC_QR_BACKUP = 251,
DC_QR_ERROR = 400,
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
@@ -153,8 +149,6 @@ export enum C {
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
DC_STR_BACKUP_TRANSFER_QR = 162,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
@@ -295,7 +289,6 @@ export const EventId2EventName: { [key: number]: string } = {
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
106: 'DC_EVENT_IMAP_INBOX_IDLE',
150: 'DC_EVENT_NEW_BLOB_FILE',
151: 'DC_EVENT_DELETED_BLOB_FILE',
300: 'DC_EVENT_WARNING',
@@ -309,7 +302,6 @@ export const EventId2EventName: { [key: number]: string } = {
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
2016: 'DC_EVENT_MSG_DELETED',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',

View File

@@ -446,8 +446,7 @@ describe('Offline Tests with unconfigured account', function () {
context.setChatProfileImage(chatId, imagePath)
const blobPath = context.getChat(chatId).getProfileImage()
expect(blobPath.startsWith(blobs)).to.be.true
expect(blobPath.includes('image')).to.be.true
expect(blobPath.endsWith('.jpeg')).to.be.true
expect(blobPath.endsWith(image)).to.be.true
context.setChatProfileImage(chatId, null)
expect(context.getChat(chatId).getProfileImage()).to.be.equal(

View File

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

View File

@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE

View File

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

View File

@@ -2,34 +2,34 @@
high level API reference
========================
- :class:`deltachat.Account` (your main entry point, creates the
- :class:`deltachat.account.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.Contact`
- :class:`deltachat.Chat`
- :class:`deltachat.Message`
- :class:`deltachat.contact.Contact`
- :class:`deltachat.chat.Chat`
- :class:`deltachat.message.Message`
Account
-------
.. autoclass:: deltachat.Account
.. autoclass:: deltachat.account.Account
:members:
Contact
-------
.. autoclass:: deltachat.Contact
.. autoclass:: deltachat.contact.Contact
:members:
Chat
----
.. autoclass:: deltachat.Chat
.. autoclass:: deltachat.chat.Chat
:members:
Message
-------
.. autoclass:: deltachat.Message
.. autoclass:: deltachat.message.Message
:members:

View File

@@ -32,13 +32,25 @@ class GroupTrackingPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
print(
"ac_member_added {} to chat {} from {}".format(
contact.addr,
chat.id,
actor or message.get_sender_contact().addr,
),
)
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
print(
"ac_member_removed {} from chat {} by {}".format(
contact.addr,
chat.id,
actor or message.get_sender_contact().addr,
),
)
def main(argv=None):

View File

@@ -40,7 +40,7 @@ def test_echo_quit_plugin(acfactory, lp):
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking)
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
ac1, ac2 = acfactory.get_online_accounts(2)

View File

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

View File

@@ -11,11 +11,10 @@ authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email",
"Topic :: Software Development :: Libraries",
]
@@ -34,7 +33,6 @@ dynamic = [
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
"Mastodon" = "https://chaos.social/@delta"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"

View File

@@ -6,7 +6,7 @@ from array import array
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
from . import const, hookspec
from .capi import ffi, lib
@@ -376,22 +376,6 @@ class Account:
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
def _wait_next_message_ids(self) -> List[int]:
"""Return IDs of all next messages from all chats."""
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
def wait_next_incoming_message(self) -> Message:
"""Waits until the next incoming message
with ID higher than given is received and returns it."""
while True:
message_ids = self._wait_next_message_ids()
for msg_id in message_ids:
message = Message.from_db(self, msg_id)
if message and not message.is_from_self() and not message.is_from_device():
self.set_config("last_msg_id", str(msg_id))
return message
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat()
@@ -427,7 +411,7 @@ class Account:
assert dc_chatlist != ffi.NULL
chatlist = []
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
chatlist.append(Chat(self, chat_id))
return chatlist

View File

@@ -71,9 +71,9 @@ class Contact:
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self) -> bool:
def is_verified(self):
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) == 2
return lib.dc_contact_is_verified(self._dc_contact)
def get_verifier(self, contact):
"""Return the address of the contact that verified the contact."""

View File

@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
for i in range(lib.dc_array_get_cnt(dc_array_t)):
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i))

View File

@@ -9,11 +9,11 @@ from contextlib import contextmanager
from queue import Empty, Queue
from . import const
from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
from .account import Account
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
@@ -190,7 +190,7 @@ class FFIEventTracker:
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent
"""
self.get_matching("DC_EVENT_IMAP_INBOX_IDLE")
self.get_info_contains("INBOX: Idle entering")
def wait_next_incoming_message(self):
"""wait for and return next incoming message."""

View File

@@ -344,16 +344,6 @@ class Message:
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self.account, contact_id)
def is_from_self(self):
"""Return true if the message is sent by self."""
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return contact_id == const.DC_CONTACT_ID_SELF
def is_from_device(self):
"""Return true if the message is sent by the device."""
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return contact_id == const.DC_CONTACT_ID_DEVICE
#
# Message State query methods
#
@@ -486,9 +476,6 @@ class Message:
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
return lib.dc_msg_get_download_state(dc_msg)
def download_full(self) -> None:
lib.dc_download_full_msg(self.account._dc_context, self.id)
# some code for handling DC_MSG_* view types
@@ -510,7 +497,8 @@ def get_viewtype_code_from_name(view_type_name):
if code is not None:
return code
raise ValueError(
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
"message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
)

View File

@@ -9,7 +9,7 @@ import threading
import time
import weakref
from queue import Queue
from typing import Callable, Dict, List, Optional, Set
from typing import Callable, List, Optional, Dict, Set
import pytest
import requests
@@ -137,9 +137,6 @@ def pytest_report_header(config, startdir):
@pytest.fixture(scope="session")
def testprocess(request):
"""Return live account configuration manager.
The returned object is a :class:`TestProcess` object."""
return TestProcess(pytestconfig=request.config)
@@ -234,8 +231,6 @@ def write_dict_to_dir(dic, target_dir):
@pytest.fixture()
def data(request):
"""Test data."""
class Data:
def __init__(self) -> None:
# trying to find test data heuristically
@@ -619,7 +614,6 @@ class ACFactory:
@pytest.fixture()
def acfactory(request, tmpdir, testprocess, data):
"""Account factory."""
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
@@ -685,14 +679,11 @@ class BotProcess:
@pytest.fixture()
def tmp_db_path(tmpdir):
"""Return a path inside the temporary directory where the database can be created."""
return tmpdir.join("test.db").strpath
@pytest.fixture()
def lp():
"""Log printer fixture."""
class Printer:
def sec(self, msg: str) -> None:
print()

View File

@@ -1,6 +1,6 @@
from queue import Queue
from threading import Event
from typing import TYPE_CHECKING, List
from typing import List, TYPE_CHECKING
from .hookspec import Global, account_hookimpl

View File

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

View File

@@ -15,6 +15,6 @@ class TestEmpty:
def test_prepare_setup_measurings(self, acfactory):
acfactory.get_online_accounts(BENCH_NUM)
@pytest.mark.parametrize("num", range(BENCH_NUM + 1))
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
def test_setup_online_accounts(self, acfactory, num):
acfactory.get_online_accounts(num)

View File

@@ -8,7 +8,7 @@ import pytest
import deltachat
def test_db_busy_error(acfactory):
def test_db_busy_error(acfactory, tmpdir):
starttime = time.time()
log_lock = threading.RLock()

View File

@@ -220,16 +220,16 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
acfactory.bring_accounts_online()
assert_folders_configured(ac1)
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
assert ac1.direct_imap.select_config_folder("mvbox" if mvbox_move else "inbox")
with ac1.direct_imap.idle() as idle1:
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
assert_folders_configured(ac1)
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
if mvbox_move:
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
assert idle1.wait_for_seen()
assert_folders_configured(ac1)
lp.sec("create a cloned ac1 and fetch contact history during configure")
@@ -271,12 +271,12 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
ac1._evtracker.wait_next_incoming_message()
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
# wait until the bcc_self message arrives
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
with ac1.direct_imap.idle() as idle1:
ac1.set_config("bcc_self", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
# wait until the bcc_self message arrives
assert idle1.wait_for_seen()
lp.sec("Clone online account and let it fetch the existing messages")
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
@@ -494,7 +494,7 @@ def test_multidevice_sync_seen(acfactory, lp):
assert "Expires: " in ac1_clone_message.get_message_info()
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
@@ -507,10 +507,9 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
for ac in [ac1, ac1_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
dir = tmp_path / "exportdir"
dir.mkdir()
ac1.export_self_keys(str(dir))
ac1_offl.import_self_keys(str(dir))
dir = tmpdir.mkdir("exportdir")
ac1.export_self_keys(dir.strpath)
ac1_offl.import_self_keys(dir.strpath)
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
@@ -542,7 +541,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
ac1.set_config("bcc_self", "0")
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
@@ -557,10 +556,9 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
dir = tmp_path / "exportdir"
dir.mkdir()
ac2.export_self_keys(str(dir))
ac2_offl.import_self_keys(str(dir))
dir = tmpdir.mkdir("exportdir")
ac2.export_self_keys(dir.strpath)
ac2_offl.import_self_keys(dir.strpath)
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")

View File

@@ -2,18 +2,18 @@ import os
import queue
import sys
import time
import base64
from datetime import datetime, timezone
import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat import const
from deltachat.hookspec import account_hookimpl
from deltachat.message import Message
from deltachat.tracker import ImexTracker
def test_basic_imap_api(acfactory, tmp_path):
def test_basic_imap_api(acfactory, tmpdir):
ac1, ac2 = acfactory.get_online_accounts(2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
@@ -28,7 +28,7 @@ def test_basic_imap_api(acfactory, tmp_path):
imap2.mark_all_read()
assert imap2.get_unread_cnt() == 0
imap2.dump_imap_structures(tmp_path, logfile=sys.stdout)
imap2.dump_imap_structures(tmpdir, logfile=sys.stdout)
imap2.shutdown()
@@ -36,29 +36,29 @@ def test_basic_imap_api(acfactory, tmp_path):
def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys.
acfactory.remove_preconfigured_keys()
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519))
acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2.wait_next_incoming_message()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1.wait_next_incoming_message()
msg2_in = ac1._evtracker.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2.wait_next_incoming_message()
msg3_in = ac2._evtracker.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
@@ -72,37 +72,35 @@ def test_configure_canceled(acfactory):
pass
def test_configure_unref(tmp_path):
def test_configure_unref(tmpdir):
"""Test that removing the last reference to the context during ongoing configuration
does not result in use-after-free."""
from deltachat.capi import ffi, lib
path = tmp_path / "test_configure_unref"
path.mkdir()
dc_context = lib.dc_context_new(ffi.NULL, str(path / "dc.db").encode("utf8"), ffi.NULL)
path = tmpdir.mkdir("test_configure_unref").join("dc.db").strpath
dc_context = lib.dc_context_new(ffi.NULL, path.encode("utf8"), ffi.NULL)
lib.dc_set_config(dc_context, "addr".encode("utf8"), "foo@x.testrun.org".encode("utf8"))
lib.dc_set_config(dc_context, "mail_pw".encode("utf8"), "abc".encode("utf8"))
lib.dc_configure(dc_context)
lib.dc_context_unref(dc_context)
def test_export_import_self_keys(acfactory, tmp_path, lp):
def test_export_import_self_keys(acfactory, tmpdir, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
dir = tmp_path / "exportdir"
dir.mkdir()
export_files = ac1.export_self_keys(str(dir))
dir = tmpdir.mkdir("exportdir")
export_files = ac1.export_self_keys(dir.strpath)
assert len(export_files) == 2
for x in export_files:
assert x.startswith(str(dir))
assert x.startswith(dir.strpath)
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)")
for name in dir.iterdir():
lp.indent(str(dir / name))
for name in os.listdir(dir.strpath):
lp.indent(dir.strpath + os.sep + name)
lp.sec("importing into existing account")
ac2.import_self_keys(str(dir))
ac2.import_self_keys(dir.strpath)
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
assert key_id2 == key_id
@@ -136,87 +134,79 @@ def test_one_account_send_bcc_setting(acfactory, lp):
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
with ac1.direct_imap.idle() as idle1:
msg_out = chat.send_text("message2")
# wait for send out (BCC)
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# wait for send out (BCC)
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
# now make sure we are sending message to ourselves too
assert self_addr in ev.data2
assert other_addr in ev.data2
assert idle1.wait_for_seen()
# Second client receives only second message, but not the first
ev_msg = ac1_clone._evtracker.wait_next_messages_changed()
assert ev_msg.text == msg_out.text
# now make sure we are sending message to ourselves too
assert self_addr in ev.data2
assert other_addr in ev.data2
# BCC-self messages are marked as seen by the sender device.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
# Check that the message is marked as seen on IMAP.
ac1.direct_imap.select_folder("Inbox")
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "somedäüta"
ext = ".html.zip"
p = tmp_path / (basename + ext)
p.write_text("some data")
basename = "somedäüta.html.zip"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
f.write("some data")
def send_and_receive_message():
lp.sec("ac1: prepare and send attachment + text to ac2")
msg1 = Message.new_empty(ac1, "file")
msg1.set_text("withfile")
msg1.set_file(str(p))
msg1.set_file(p)
chat.send_msg(msg1)
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
return ac2.get_message_by_id(ev.data2)
msg = send_and_receive_message()
assert msg.text == "withfile"
assert open(msg.filename).read() == "some data"
msg.filename.index(basename)
assert msg.filename.endswith(ext)
assert msg.filename.endswith(basename)
msg2 = send_and_receive_message()
assert msg2.text == "withfile"
assert open(msg2.filename).read() == "some data"
msg2.filename.index(basename)
assert msg2.filename.endswith(ext)
assert msg2.filename.endswith("html.zip")
assert msg.filename != msg2.filename
def test_send_file_html_attachment(tmp_path, acfactory, lp):
def test_send_file_html_attachment(tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "test"
ext = ".html"
basename = "test.html"
content = "<html><body>text</body>data"
p = tmp_path / (basename + ext)
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
p.write_text(content)
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
f.write(content)
lp.sec("ac1: prepare and send attachment + text to ac2")
chat.send_file(str(p), mime_type="text/html")
chat.send_file(p, mime_type="text/html")
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
msg = ac2.get_message_by_id(ev.data2)
assert open(msg.filename).read() == content
msg.filename.index(basename)
assert msg.filename.endswith(ext)
assert msg.filename.endswith(basename)
def test_html_message(acfactory, lp):
@@ -324,62 +314,6 @@ def test_webxdc_message(acfactory, data, lp):
assert msg2.text == "message1"
assert msg2.is_webxdc()
assert msg2.filename
ac2._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
ac2.direct_imap.select_folder("Inbox")
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_webxdc_huge_update(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
msg1 = Message.new_empty(ac1, "webxdc")
msg1.set_text("message1")
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
msg1 = chat.send_msg(msg1)
assert msg1.is_webxdc()
assert msg1.filename
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.is_webxdc()
payload = "A" * 1000
assert msg1.send_status_update({"payload": payload}, "some test data")
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
update = msg2.get_status_updates()[0]
assert update["payload"] == payload
def test_webxdc_download_on_demand(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.introduce_each_other([ac1, ac2])
chat = acfactory.get_accepted_chat(ac1, ac2)
msg1 = Message.new_empty(ac1, "webxdc")
msg1.set_text("message1")
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
msg1 = chat.send_msg(msg1)
assert msg1.is_webxdc()
assert msg1.filename
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.is_webxdc()
lp.sec("ac2 sets download limit")
ac2.set_config("download_limit", "100")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
ac2_update = ac2._evtracker.wait_next_incoming_message()
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert not msg2.get_status_updates()
ac2_update.download_full()
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
assert msg2.get_status_updates()
# Get a event notifying that the message disappeared from the chat.
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert msgs_changed_event.data1 == msg2.chat.id
assert msgs_changed_event.data2 == 0
def test_mvbox_sentbox_threads(acfactory, lp):
@@ -409,42 +343,7 @@ def test_move_works(acfactory):
# Message is downloaded
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
def test_move_avoids_loop(acfactory):
"""Test that the message is only moved once.
This is to avoid busy loop if moved message reappears in the Inbox
or some scanned folder later.
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
"""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
ac1_chat.send_text("Message 1")
# Message is moved to the DeltaChat folder and downloaded.
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg1.text == "Message 1"
# Move the message to the INBOX again.
ac2.direct_imap.select_folder("DeltaChat")
ac2.direct_imap.conn.move(["*"], "INBOX")
ac1_chat.send_text("Message 2")
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg2.text == "Message 2"
# Check that Message 1 is still in the INBOX folder
# and Message 2 is in the DeltaChat folder.
ac2.direct_imap.select_folder("INBOX")
assert len(ac2.direct_imap.get_all_messages()) == 1
ac2.direct_imap.select_folder("DeltaChat")
assert len(ac2.direct_imap.get_all_messages()) == 1
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
def test_move_works_on_self_sent(acfactory):
@@ -489,12 +388,9 @@ def test_forward_messages(acfactory, lp):
lp.sec("ac2: check new chat has a forwarded message")
assert chat3.is_promoted()
messages = chat3.get_messages()
assert len(messages) == 1
msg = messages[-1]
assert msg.is_forwarded()
ac2.delete_messages(messages)
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_DELETED")
assert ev.data2 == messages[0].id
assert not chat3.get_messages()
@@ -617,22 +513,22 @@ def test_send_and_receive_message_markseen(acfactory, lp):
msg4 = ac2._evtracker.wait_next_incoming_message()
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg2, msg4])
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert msg2.chat.id == msg4.chat.id
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
ac2._evtracker.get_info_contains("Marked messages .* in folder INBOX as seen.")
with ac1.direct_imap.idle() as idle1:
with ac2.direct_imap.idle() as idle2:
ac2.mark_seen_messages([msg2, msg4])
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert msg2.chat.id == msg4.chat.id
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
idle2.wait_for_seen()
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
# Check that ac1 marks the read receipt as read.
ac1._evtracker.get_info_contains("Marked messages .* in folder INBOX as seen.")
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
idle1.wait_for_seen() # Check that ac1 marks the read receipt as read
assert msg1.is_out_mdn_received()
assert msg3.is_out_mdn_received()
@@ -663,8 +559,7 @@ def test_moved_markseen(acfactory):
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
msg = ac2._evtracker.wait_next_incoming_message()
# Accept the contact request.
msg.chat.accept()
@@ -718,24 +613,18 @@ def test_markseen_message_and_mdn(acfactory, mvbox_move):
# Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
ac2.mark_seen_messages([msg])
folder = "mvbox" if mvbox_move else "inbox"
for ac in [ac1, ac2]:
if mvbox_move:
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
ac1.direct_imap.select_config_folder(folder)
ac2.direct_imap.select_config_folder(folder)
with ac1.direct_imap.idle() as idle1:
with ac2.direct_imap.idle() as idle2:
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
# Check that the mdn is marked as seen
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
# Check original message is marked as seen
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
ac2.mark_seen_messages([msg])
idle2.wait_for_seen() # Check original message is marked as seen
idle1.wait_for_seen() # Check that the mdn is marked as seen
def test_reply_privately(acfactory):
@@ -789,24 +678,23 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
ac1.direct_imap.select_config_folder("mvbox")
with ac1.direct_imap.idle() as idle1:
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
lp.sec("ac1: waiting for incoming activity")
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
lp.sec("ac1: waiting for incoming activity")
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
# Wait for the message to be marked as seen on IMAP.
assert idle1.wait_for_seen()
# MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received()
ac1.direct_imap.select_config_folder("mvbox")
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1300,7 +1188,7 @@ def test_quote_encrypted(acfactory, lp):
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
def test_quote_attachment(tmp_path, acfactory, lp):
def test_quote_attachment(tmpdir, acfactory, lp):
"""Test that replies with an attachment and a quote are received correctly."""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1315,14 +1203,15 @@ def test_quote_attachment(tmp_path, acfactory, lp):
assert received_message.text == "hi"
basename = "attachment.txt"
p = tmp_path / basename
p.write_text("data to send")
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
f.write("data to send")
lp.sec("ac2 sends a reply to ac1")
chat2 = received_message.create_chat()
reply = Message.new_empty(ac2, "file")
reply.set_text("message reply")
reply.set_file(str(p))
reply.set_file(p)
reply.quote = received_message
chat2.send_msg(reply)
@@ -1429,7 +1318,7 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
@@ -1464,9 +1353,10 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
path = tmpdir.join("large")
with open(path, "wb") as fout:
fout.write(os.urandom(download_limit + 1))
msgs.append(chat.send_file(path.strpath))
lp.sec("sending a reaction to the large message from ac1 to ac2")
react_str = "\N{THUMBS UP SIGN}"
@@ -1479,7 +1369,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
@@ -1525,7 +1415,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
assert reactions.get_by_contact(contacts[0]) == react_str
def test_import_export_online_all(acfactory, tmp_path, data, lp):
def test_import_export_online_all(acfactory, tmpdir, data, lp):
(ac1,) = acfactory.get_online_accounts(1)
lp.sec("create some chat content")
@@ -1537,10 +1427,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
path = tmp_path / "attachment.txt"
with path.open("w") as file:
path = tmpdir.join("attachment.txt")
with open(path, "w") as file:
file.truncate(100000)
chat1.send_file(str(path))
chat1.send_file(path.strpath)
def assert_account_is_proper(ac):
contacts = ac.get_contacts(query="some1")
@@ -1558,13 +1448,12 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
assert_account_is_proper(ac1)
backupdir = tmp_path / "backup"
backupdir.mkdir()
backupdir = tmpdir.mkdir("backup")
lp.sec(f"export all to {backupdir}")
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
@@ -1582,7 +1471,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
ac2 = acfactory.get_unconfigured_account()
lp.sec("get latest backup file")
path2 = ac2.get_latest_backupfile(str(backupdir))
path2 = ac2.get_latest_backupfile(backupdir.strpath)
assert path2 == path
lp.sec("import backup and check it's proper")
@@ -1600,10 +1489,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
lp.sec(f"Second-time export all to {backupdir}")
ac1.stop_io()
path2 = ac1.export_all(str(backupdir))
path2 = ac1.export_all(backupdir.strpath)
assert os.path.exists(path2)
assert path2 != path
assert ac2.get_latest_backupfile(str(backupdir)) == path2
assert ac2.get_latest_backupfile(backupdir.strpath) == path2
def test_ac_setup_message(acfactory, lp):
@@ -1679,69 +1568,6 @@ def test_qr_join_chat(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
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()
# Receive "Hello!" message.
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
when the database already contained the contact with a different email address capitalization.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac1 adds ac2 as a contact with an email address in uppercase.
ac2_addr_uppercase = ac2.get_config("addr").upper()
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
ac1.create_contact(ac2_addr_uppercase)
lp.sec("ac3 creates a verified group with a QR code")
chat = ac3.create_group_chat("hello", verified=True)
qr = chat.get_join_qr()
lp.sec("ac1 joins a verified group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code")
ac2.qr_join_chat(qr)
ac1._evtracker.wait_next_incoming_message()
# ac1 should see both ac3 and ac2 as verified.
assert len(ac1_chat.get_contacts()) == 3
for contact in ac1_chat.get_contacts():
assert contact.is_verified()
def test_set_get_contact_avatar(acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1958,15 +1784,15 @@ def test_connectivity(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test stop_io() and start_io()")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
@@ -1987,8 +1813,8 @@ def test_connectivity(acfactory, lp):
ac2.create_chat(ac1).send_text("Hi 2")
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
@@ -1998,7 +1824,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -2013,7 +1839,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -2022,10 +1848,10 @@ def test_connectivity(acfactory, lp):
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(acfactory, lp):
@@ -2508,9 +2334,9 @@ def test_archived_muted_chat(acfactory, lp):
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
while 1:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK:
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
assert ev.data2 == 0
archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK)
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
assert archive.count_fresh_messages() == 1
assert chat2.count_fresh_messages() == 1
break

View File

@@ -30,28 +30,28 @@ def wait_msgs_changed(account, msgs_list):
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmp_path, acfactory, lp):
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating in-creation file outside of blobdir")
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.touch()
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join("file.txt").ensure(file=1)
with pytest.raises(Exception):
chat.prepare_message_file(str(src))
chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating file outside of blobdir")
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.write_text("hello there\n")
msg = chat.send_file(str(src))
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
assert msg.filename.endswith(".txt")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join("file.txt")
src.write("hello there\n")
chat.send_file(src.strpath)
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)

View File

@@ -4,11 +4,12 @@ from datetime import datetime, timedelta, timezone
import pytest
import deltachat as dc
from deltachat import Account, const
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from deltachat.hookspec import account_hookimpl
from deltachat.message import Message
from deltachat.tracker import ImexFailed
from deltachat import Account, account_hookimpl, Message
@pytest.mark.parametrize(
@@ -51,18 +52,18 @@ def test_parse_system_add_remove(msgtext, res):
class TestOfflineAccountBasic:
def test_wrong_db(self, tmp_path):
p = tmp_path / "hello.db"
p.write_text("123")
account = Account(str(p))
def test_wrong_db(self, tmpdir):
p = tmpdir.join("hello.db")
p.write("123")
account = Account(p.strpath)
assert not account.is_open()
def test_os_name(self, tmp_path):
p = tmp_path / "hello.db"
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
ac1 = Account(str(p), os_name="solarpunk")
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, data):
@@ -298,13 +299,13 @@ class TestOfflineChat:
assert not d["draft"] if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
@@ -480,19 +481,6 @@ class TestOfflineChat:
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
assert contact2.name == "real"
def test_send_lots_of_offline_msgs(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac1.set_config("configured_mail_server", "example.org")
ac1.set_config("configured_mail_user", "example.org")
ac1.set_config("configured_mail_pw", "example.org")
ac1.set_config("configured_send_server", "example.org")
ac1.set_config("configured_send_user", "example.org")
ac1.set_config("configured_send_pw", "example.org")
ac1.start_io()
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
for i in range(50):
chat.send_text("hello")
def test_create_chat_simple(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
@@ -508,22 +496,22 @@ class TestOfflineChat:
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path):
backupdir = tmp_path / "backup"
backupdir.mkdir()
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not list(backupdir.iterdir())
assert not backupdir.listdir()
ac1.stop_io()
path = ac1.export_all(str(backupdir))
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
@@ -537,27 +525,27 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
def test_import_export_on_encrypted_acct(self, acfactory, tmpdir):
passphrase1 = "passphrase1"
passphrase2 = "passphrase2"
backupdir = tmp_path / "backup"
backupdir.mkdir()
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not list(backupdir.iterdir())
assert not backupdir.listdir()
ac1.stop_io()
path = ac1.export_all(str(backupdir))
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account(closed=True)
@@ -592,27 +580,27 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_with_passphrase(self, acfactory, tmp_path):
def test_import_export_with_passphrase(self, acfactory, tmpdir):
passphrase = "test_passphrase"
wrong_passphrase = "wrong_passprase"
backupdir = tmp_path / "backup"
backupdir.mkdir()
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not list(backupdir.iterdir())
assert not backupdir.listdir()
ac1.stop_io()
path = ac1.export_all(str(backupdir), passphrase)
path = ac1.export_all(backupdir.strpath, passphrase)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
@@ -631,7 +619,7 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir):
"""
Test that account passphrase isn't lost if backup failed to be imported.
See https://github.com/deltachat/deltachat-core-rust/issues/3379
@@ -639,24 +627,24 @@ class TestOfflineChat:
acct_passphrase = "passphrase1"
bak_passphrase = "passphrase2"
wrong_passphrase = "wrong_passprase"
backupdir = tmp_path / "backup"
backupdir.mkdir()
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmp_path / "some.bin"
bin.write_bytes(b"\00123" * 10000)
msg = chat.send_file(str(bin))
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not list(backupdir.iterdir())
assert not backupdir.listdir()
ac1.stop_io()
path = ac1.export_all(str(backupdir), bak_passphrase)
path = ac1.export_all(backupdir.strpath, bak_passphrase)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account(closed=True)
@@ -817,7 +805,7 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref,
)
assert len(list(iter_array(dc_array, lambda x: x))) == 2

View File

@@ -1,7 +1,7 @@
import os
from queue import Queue
import deltachat as dc
from deltachat import capi, cutil, register_global_plugin
from deltachat import capi, const, cutil, register_global_plugin
from deltachat.capi import ffi, lib
from deltachat.hookspec import global_hookimpl
from deltachat.testplugin import (
@@ -9,7 +9,6 @@ from deltachat.testplugin import (
create_dict_from_files_in_path,
write_dict_to_dir,
)
from deltachat.cutil import from_optional_dc_charpointer
# from deltachat.account import EventLogger
@@ -65,17 +64,16 @@ class TestACSetup:
assert pc._account2state[ac1] == pc.IDLEREADY
assert pc._account2state[ac2] == pc.IDLEREADY
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path):
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
ac1 = acfactory.get_pseudo_configured_account()
holder = acfactory._acsetup.testprocess
assert holder.cache_maybe_store_configured_db_files(ac1)
assert not holder.cache_maybe_store_configured_db_files(ac1)
acdir = tmp_path / "newaccount"
acdir.mkdir()
acdir = tmpdir.mkdir("newaccount")
addr = ac1.get_config("addr")
target_db_path = acdir / "db"
assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path))
assert sum(1 for _ in acdir.iterdir()) >= 2
target_db_path = acdir.join("db").strpath
assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
assert len(os.listdir(acdir)) >= 2
def test_liveconfig_caching(acfactory, monkeypatch):
@@ -113,40 +111,40 @@ def test_dc_close_events(acfactory):
shutdowns.get(timeout=2)
def test_wrong_db(tmp_path):
p = tmp_path / "hello.db"
def test_wrong_db(tmpdir):
p = tmpdir.join("hello.db")
# write an invalid database file
p.write_bytes(b"x123" * 10)
p.write("x123" * 10)
context = lib.dc_context_new(ffi.NULL, str(p).encode("ascii"), ffi.NULL)
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
assert not lib.dc_context_is_open(context)
def test_empty_blobdir(tmp_path):
db_fname = tmp_path / "hello.db"
def test_empty_blobdir(tmpdir):
db_fname = tmpdir.join("hello.db")
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), b""),
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
lib.dc_context_unref,
)
assert ctx != ffi.NULL
def test_event_defines():
assert dc.const.DC_EVENT_INFO == 100
assert dc.const.DC_CONTACT_ID_SELF
assert const.DC_EVENT_INFO == 100
assert const.DC_CONTACT_ID_SELF
def test_sig():
sig = capi.lib.dc_event_has_string_data
assert not sig(dc.const.DC_EVENT_MSGS_CHANGED)
assert sig(dc.const.DC_EVENT_INFO)
assert sig(dc.const.DC_EVENT_WARNING)
assert sig(dc.const.DC_EVENT_ERROR)
assert sig(dc.const.DC_EVENT_SMTP_CONNECTED)
assert sig(dc.const.DC_EVENT_IMAP_CONNECTED)
assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN)
assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
@@ -175,10 +173,10 @@ def test_provider_info_none():
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_open(tmp_path):
db_fname = tmp_path / "test.db"
def test_get_info_open(tmpdir):
db_fname = tmpdir.join("test.db")
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), ffi.NULL),
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
@@ -217,19 +215,3 @@ def test_logged_ac_process_ffi_failure(acfactory):
assert "ac_process_ffi_event" in res
assert "ZeroDivisionError" in res
assert "Traceback" in res
def test_jsonrpc_blocking_call(tmp_path):
accounts_fname = tmp_path / "accounts"
accounts = ffi.gc(
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 = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == "true"
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

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

View File

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

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